Skip to content

feat(mcp): distribute team MCP servers as a seventh resource type - #252

Merged
jeff-r2026 merged 5 commits into
mainfrom
feat/mcp-distribution
Jul 28, 2026
Merged

feat(mcp): distribute team MCP servers as a seventh resource type#252
jeff-r2026 merged 5 commits into
mainfrom
feat/mcp-distribution

Conversation

@jeff-r2026

@jeff-r2026 jeff-r2026 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #251. Adds mcp as a seventh resource type: declare servers once in mcp/mcp.yaml, and teamai pull writes each installed tool's native config.

# mcp/mcp.yaml
servers:
  - name: gpu-analysis
    transport: http
    url: https://gpubiz.woa.com/api/ai/mcp
    headers:
      Authorization: Bearer ${GPU_ANALYSIS_TOKEN}
  - name: context7
    transport: stdio
    command: npx
    args: ['-y', '@upstash/context7-mcp']
    requires: [npx]

Why teamai rather than a committed .mcp.json

A committed .mcp.json only covers Claude Code at project scope, and cannot hold a secret. Other tools each want a different file shape; two of them are files teamai does not own (~/.claude.json, ~/.codex/config.toml).

The engine never rewrites a whole file. It edits only the server keys it installed (~/.teamai/managed-mcp.json), writes atomically, and patches Codex at the text level so comments survive. Name collisions with hand-added servers skip unless --force.

Secrets

Write ${VAR}, never a literal. Resolve from the environment, then from env/env.yaml~/.teamai/env.

Where a tool can keep secrets off its own file, teamai does:

  • Claude project .mcp.json keeps the placeholder (Claude expands it).
  • Codex names the variable (bearer_token_env_var / env_http_headers) instead of storing the value.

Anything else gets a resolved value at 0600. In project scope, tools that cannot expand ${VAR} skip secret-bearing servers rather than writing plaintext into a committed file.

Fixes from end-to-end runs

Unit tests passed; these came from real tools:

  1. Project scope secret leak — resolving ${VAR} for tools that cannot expand it wrote plaintext into repo-tracked files. Now fail-closed.
  2. Project scope dead paths — no fallback from mcpProject to the user-scope path; absent mcpProject means no project-scope support.
  3. Codex HTTP — Codex supports streamable HTTP (--url / bearer_token_env_var); only sse is skipped. The old “skip http” test encoded the wrong assumption.

Paths

Tool User scope Project scope
claude ~/.claude.json <root>/.mcp.json
cursor ~/.cursor/mcp.json <root>/.cursor/mcp.json
codebuddy / workbuddy ~/.<tool>/mcp.json <root>/.<tool>/mcp.json
codex ~/.codex/config.toml not supported

Test plan

1808 unit tests, tsc --noEmit, and npm run build pass. Items below were run against a real team repo and a live HTTP MCP server, using each tool's own command.

  • User scope: teamai pull → Connected via claude / codebuddy mcp list
  • Codex: codex exec listed tools from the http server; no plaintext token in config.toml; inject-then-remove restores the file byte-for-byte
  • Project scope: .mcp.json keeps ${VAR}; Connected in Claude; appears only inside the project (user scope cleared)
  • Unsetting the token → HTTP 401 (proves expansion)
  • Project-scope secret policy: cursor/codebuddy skip with a reason; no plaintext in the project
  • sse still skipped for Codex
  • teamai mcp remove / uninstall leave hand-added servers alone

Claude may show project .mcp.json servers as pending approval until accepted once interactively — documented, not bypassed.

Docs

README kept short; usage-guide (both languages) covers the matrix, secrets, and project-scope policy.

Adds `mcp` as a seventh resource type. Teams declare MCP servers once in
mcp/mcp.yaml and `teamai pull` writes them into each tool's own config in
that tool's native format: ~/.claude.json, ~/.cursor/mcp.json,
~/.codebuddy/mcp.json, ~/.workbuddy/mcp.json and ~/.codex/config.toml.

The target files are not ours — ~/.claude.json also holds the OAuth session
and all per-project state — so every write is key-level surgery on the
existing document via atomic temp+rename, and an unparseable file aborts the
injection instead of being rebuilt. Codex is spliced at the text level
because a whole-file round-trip through a TOML parser silently drops every
user comment.

MCP entries have no field we can stamp a marker into, so ownership lives in
~/.teamai/managed-mcp.json, mirroring how hooks already track Cursor/Codex
entries. Only keys the manifest claims are ever rewritten or removed; a team
server colliding with a user's own name is skipped rather than overwritten.

Secrets stay out of the repo: values are written as ${VAR} and resolved at
inject time from the environment or the team's env channel. A server with an
unresolved variable is skipped with a hint, since one that cannot connect
would fail on every session start. Transport and runtime support is checked
up front for the same reason — Codex gets stdio servers only.
tclaude ships Claude Code with `customUserDataDir: .tclaude`, relocating the
whole user data dir — so its MCP config is ~/.tclaude/.claude.json, not the
~/.tclaude.json that the Claude naming convention would suggest. Verified
against the installed package rather than inferred.

No mcpProject: project scope for the Claude family is <root>/.mcp.json, which
the `claude` target already writes and tclaude reads from the same path.

Confirmed end to end with `tclaude mcp list` reporting the injected server as
Connected, with tclaude's 22 pre-existing config keys untouched.
…iles

Project scope was only exercised by unit tests until an end-to-end run on a
real project surfaced two problems.

Resolving a ${VAR} for a tool that cannot expand it wrote the token as
plaintext. At user scope that file stays out of git, but a project-scope file
sits in the repo and gets committed, so the secret would reach version
control. Only Claude Code expands ${VAR}, so it keeps receiving the
placeholder; for every other tool a server referencing a variable is now
skipped with the reason, rather than silently leaked.

Target resolution also fell back from mcpProject to the user-scope path, which
put a <root>/.codex/config.toml and a <root>/.tclaude/.claude.json into the
project that neither tool ever reads. The two scopes name different things, so
the fallback is gone: absent mcpProject now means the tool has no project-scope
support, matching what the code comments and docs already claimed.
Codex was assumed to model every MCP server as a launched process, so http and
sse were both skipped for it. That was checked against codex-cli 0.142.5 and is
wrong for http: `codex mcp add --url` configures a streamable HTTP server, and
`transport: streamable_http` shows up in `codex mcp get`. Only sse has no
counterpart, so only sse is still skipped.

Codex also keeps secrets out of config.toml, by naming the environment variable
rather than storing its value. `Authorization: Bearer ${TOKEN}` therefore maps
to `bearer_token_env_var = "TOKEN"`, and any other whole-value placeholder to an
`env_http_headers` entry. Codex re-adds the "Bearer " prefix itself — confirmed
by reaching a real server whose token lives only in the environment. What it
cannot name, such as a placeholder inside the URL, is resolved as before.

The test asserting codex skips http encoded the wrong assumption and now
asserts the mapping instead, alongside one for the resolved fallback and one
keeping sse skipped.
@jeff-r2026
jeff-r2026 merged commit 9776a0c into main Jul 28, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

提案:支持 MCP Server 分发(第 7 种资源类型 mcp

1 participant