feat(mcp): distribute team MCP servers as a seventh resource type - #252
Merged
Conversation
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.
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #251. Adds
mcpas a seventh resource type: declare servers once inmcp/mcp.yaml, andteamai pullwrites each installed tool's native config.Why teamai rather than a committed
.mcp.jsonA committed
.mcp.jsononly 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 fromenv/env.yaml→~/.teamai/env.Where a tool can keep secrets off its own file, teamai does:
.mcp.jsonkeeps the placeholder (Claude expands it).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:
${VAR}for tools that cannot expand it wrote plaintext into repo-tracked files. Now fail-closed.mcpProjectto the user-scope path; absentmcpProjectmeans no project-scope support.--url/bearer_token_env_var); onlysseis skipped. The old “skip http” test encoded the wrong assumption.Paths
~/.claude.json<root>/.mcp.json~/.cursor/mcp.json<root>/.cursor/mcp.json~/.<tool>/mcp.json<root>/.<tool>/mcp.json~/.codex/config.tomlTest plan
1808 unit tests,
tsc --noEmit, andnpm run buildpass. Items below were run against a real team repo and a live HTTP MCP server, using each tool's own command.teamai pull→ Connected viaclaude/codebuddy mcp listcodex execlisted tools from the http server; no plaintext token inconfig.toml; inject-then-remove restores the file byte-for-byte.mcp.jsonkeeps${VAR}; Connected in Claude; appears only inside the project (user scope cleared)ssestill skipped for Codexteamai mcp remove/ uninstall leave hand-added servers aloneClaude may show project
.mcp.jsonservers 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.