Skip to content

feat(mcp): keep project MCP configs with resolved tokens out of git (#882) - #886

Open
SaulMoro wants to merge 84 commits into
Tencent:mainfrom
SaulMoro:feat/882-mcp-git-exclude
Open

SaulMoro wants to merge 84 commits into
Tencent:mainfrom
SaulMoro:feat/882-mcp-git-exclude

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A project MCP config that holds a resolved token is kept out of git through the clone's own .git/info/exclude. Nothing committed changes.

 reconcileMcpForConfig (pull, session start, mcp inject)
   deliver to enabled, detected tools; write .mcp.json (0600)
+  finally, whatever delivery did (disabled tool, autoApply off, failed write, unreadable mcp.yaml):
+    for every tool's project MCP file on disk, detected or not, plus the built-in location
+    of a tool the team dropped from toolPaths or moved
+      may hold a resolved value = a teamai-owned entry still in the file whose server
+                                   needs a resolved ${VAR}, or has left mcp.yaml          # manifest
+                                 or the resolved value itself (8+ chars) in the file      # no manifest needed
+      mcp.yaml unreadable → any teamai-owned entry still in the file counts
+      if it holds one: git check-ignore
+        0 ignored → nothing · outside a repo → nothing
+        1 would commit, or git failed inside a repo → add "/.mcp.json" to .git/info/exclude   # [teamai:mcp-exclude:start] … [end], idempotent
+          under a lock (update.ts acquireLock), re-read, atomic write (tmp + rename)
+        exclude unreachable → warn with the file and git's error
 uninstall (project scope)
+  find the block in the project repo and in any nested repo holding an MCP config, every worktree
+  remove teamai's servers from every worktree
+  per repo owning a block: inspect every path it protects, in every worktree of that repo (linked ones included)
+    each one missing, or parses, holds no team server needing a resolved ${VAR}, no teamai-owned entry cleanup left,
+      and no 8+ char value of a variable set in the environment (paths, login name excepted) → remove the block, other lines kept
+    otherwise (holds one, does not parse, no tool reads it, the team's servers cannot be read)
+      → keep the block, warn naming each file and why (e.g. "it holds the value of $TEAM_BASE_URL")
 doctor
+  ✔/✖ Project MCP configs with resolved values are kept out of git   # same files and evidence as pull (resolvedValueEvidence),
+                                                                     # also when mcp.yaml is unreadable; each file once; a git error fails it
src/mcp-git-exclude.ts   carriesResolvedValue · resolvedVariableIn · gitTracking (check-ignore, 4 outcomes) · excludeFromGit (rev-parse --git-path) · findMcpGitExcludes · removeMcpGitExclude · locked atomic update
src/mcp-reconcile.ts     protection pass in a finally around the reconcile; resolvedValueEvidence (shared with doctor and uninstall); resolveMcpTargets({ includeUndetected }); desired/apply functions untouched
src/uninstall.ts         blocks (with the paths they protect) in the removal plan; removed after every worktree's servers, only where proven clean
src/doctor-delivery.ts   the new check
src/mcp-resolved-files.ts  managed-mcp-files.json per worktree: files a pull wrote a resolved value to, and servers found when a lost record was rebuilt; locked, 0600
src/types.ts             ManagedMcpRecord.resolved: the entry a pull wrote holds a resolved ${VAR}

The block is the span from the last start marker to the end marker after it, so a start marker that lost its end is left behind rather than paired with a later block's end.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature causing existing behavior to change)
  • Documentation only
  • Refactor / internal cleanup

Test Plan

npm run test:e2e: 380 passed, 26 skipped.

Tests added for the review findings (each red before its fix):

uninstall      keeps the block while .mcp.json is invalid JSON with a teamai server   was: block removed
uninstall      removes the block from a nested repo (.cursor/ with its own .git)      was: "Nothing to uninstall", block left
mcp-git-exclude orphaned start + exclude + remove → the member's lines survive        was: lines between the markers deleted
doctor         member's own `jira` (no manifest record) → no check                    was: ✖ "run teamai pull"
uninstall      managed-mcp.json gone, token still in .mcp.json → block kept           was: block removed
uninstall      teamai's server removed, member's own kept → block removed             guard: the check proves a clean file clean
uninstall      server dropped from mcp.yaml, no manifest, token set in env → kept     was: block removed
uninstall      member's own server with a $HOME path and the login name → removed     guard: the value scan skips both
pull           earlier-written file, exclude empty, then: tool disabled │ autoApply off │ tool undetected │
               mcp.yaml unparsable │ manifest gone │ another tool's write fails → listed   was: not listed
doctor         same file when: tool disabled │ undetected │ file unparsable │ git fails │ manifest gone → ✖   was: no check, or ✔
excludeFromGit check-ignore 128, exclude reachable → listed                          was: not listed, silent
excludeFromGit git fails, exclude unreachable → warns with file and git's error      was: silent
excludeFromGit outside any repository → no warning                                   guard
excludeFromGit 5 concurrent writers, slow exclude read → all 5 patterns kept          was: patterns lost
pull           server left mcp.yaml + tool disabled → listed (Claude's cleaned copy not listed)   was: not listed
pull           team drops the tool │ moves its mcpProject → old file listed         was: not listed
doctor         server left mcp.yaml + tool disabled │ tool dropped │ mcp.yaml unparsable → ✖, file named once   was: no check
doctor         claude + codebuddy share .mcp.json → named once                       was: named twice
uninstall      nested repo's linked worktree holds a token → block kept              was: block removed
uninstall      undetected tool keeps teamai's dropped server, variable unset → kept  was: block removed
uninstall      kept because of an env value → warning names $TEAM_BASE_URL           was: "remove the team's servers"

Real CLI, project scope, Bearer ${GITHUB_TOKEN}:

                      main                        this PR
git status            ?? .mcp.json  ← token       (not listed)
.git/info/exclude     —                           teamai block: /.mcp.json
second pull           —                           still one line; .gitignore unchanged
doctor                —                           ✔ … kept out of git
uninstall             —                           block removed, other lines kept

Real CLI, a tool disabled after a pull (temporary HOME, CLAUDE_CONFIG_DIR unset, team repo with Bearer ${JIRA_TOKEN}, Claude and Cursor):

1. pull                      block: /.mcp.json /.cursor/mcp.json; token in .cursor/mcp.json
2. exclude emptied (as if written before this release), disabledAgents: [cursor] in the live config
                             git status: ?? .cursor/mcp.json  ?? .mcp.json
3. doctor                    ✖ … kept out of git → names .mcp.json and .cursor/mcp.json
4. pull                      block: /.mcp.json /.cursor/mcp.json again; token still in .cursor/mcp.json; git status: (empty)
5. doctor                    ✔ … kept out of git   (delivery check for claude only: cursor is disabled)

Real CLI, a server dropped from mcp.yaml and its tool disabled after a pull (temporary HOME, CLAUDE_CONFIG_DIR unset, Claude and Cursor):

1. pull                      block: /.mcp.json /.cursor/mcp.json
2. team pushes mcp.yaml without jira; disabledAgents: [cursor]; exclude emptied (as if written before this release)
                             git status: ?? .cursor/mcp.json  ?? .mcp.json
3. doctor                    ✖ … names .mcp.json and .cursor/mcp.json, once each
4. pull                      mcp.yaml now: docs only. Claude cleaned (.mcp.json: docs, no token, not listed)
                             block: /.cursor/mcp.json (token still there, tool disabled); git status: ?? .mcp.json only
5. doctor                    ✔ … kept out of git
6. exclude emptied again → doctor ✖ names .cursor/mcp.json once (its server has left mcp.yaml)

Real CLI, uninstall with a config it cannot clean (temporary HOME, CLAUDE_CONFIG_DIR unset). mcp inject with Bearer ${JIRA_TOKEN} first, then managed-mcp.json deleted:

                            managed-mcp.json deleted                       intact
uninstall output            ⚠ Kept teamai's block in …/.git/info/exclude   ℹ Removed 1 teamai-managed MCP server(s)
                                                                           ℹ Removed teamai's MCP config entries from …/.git/info/exclude
token in .mcp.json          still there                                    gone
.git/info/exclude           block kept, scratch/ kept                      block removed, scratch/ kept
git status                  (empty)                                        ?? .mcp.json   (no token)
git check-ignore .mcp.json  ignored                                        not ignored

Same setup, .mcp.json hand-broken into invalid JSON instead:

uninstall output            ⚠ Kept teamai's block in …/.git/info/exclude
.git/info/exclude           block kept, scratch/ kept
git status                  (empty)
git check-ignore .mcp.json  ignored

Tests that close the former known limits (each red with only that step's source reverted):

literal now   tool disabled │ autoApply off, jira made a literal, token unset → listed (also Codex, older record)   was: released
nested repo   linked worktree of .cursor/ serverless → block removed │ holds a server → kept, reason named        was: kept, "no tool teamai knows reads it"
old mapping   custom mcpProject restored │ dropped │ moved → old file relisted; doctor ✖; uninstall keeps it         was: never visited
lost record   rebuild pull and two after it → line kept; uninstall names the noted server                         was: released on the second
older teamai  first pull on this version: file under a teamai.yaml revision's mapping │ CodeBuddy's old built-in
              .codebuddy/mcp.json → listed, recorded; history read once per worktree                          was: never visited
note fails    sidecar locked during the rebuild pull and the one after → line held; note lands once free      was: released
failed write  a path recorded before a write that failed │ didn't parse → record taken back                     was: kept recorded
symlinked dir .cursor → tracked config/: withheld, warning names config/mcp.json, its `git rm --cached` works    was: named .cursor/mcp.json, fix failed
              .cursor → a directory outside any repository: written, no line, doctor ✔                        was: withheld, "not a git repository"
old mapping   team removed │ renamed the server there after the remap → listed, recorded, relisted             was: judged by the new path's records, not listed
              doctor before the first pull on this version → names the old file; writes nothing               was: no check
              git tracks the old file → recorded tracked, no line; after `git rm --cached` → listed            was: never found again
moved tool    Cursor wrote to .mcp.json beside Claude, then moved → line held while a Cursor server remains    was: released by Claude's records
              same, written by an older teamai before this version → found, recorded, listed                  was: not found
shared file   Claude + CodeBuddy on .mcp.json, CodeBuddy's record lost, its server dropped, token unset → kept  was: released on Claude's record
              only Claude ever wrote a resolved value there → released once clean                              guard
              no writer list (pre-PR install), CodeBuddy's record lost, Claude's intact → kept                 was: released on Claude's record
built-in loc. Cursor moved or dropped; old .cursor/mcp.json judged as an earlier-mapped file → kept, recorded  was: judged by the new path's records
              CodeBuddy moved or dropped: its built-in .mcp.json, which Claude maps, judged for CodeBuddy too → kept  was: judged by Claude's records
no manifest   stale server dropped from mcp.yaml, token unset → listed, noted, kept; doctor ✖ before the pull   was: not found
              first pull in a worktree, a member's own server → listed, kept until it leaves (the accepted cost) was: not listed
re-included   `!/.mcp.json` in .gitignore → names that rule (source:line) and says to remove it                was: "git already tracks", `git rm --cached`
note fails    no manifest + sidecar locked → records marked unnoted in the same write; line held; retried       was: released on the next pull
              doctor, a record still marked unnoted → judged like no manifest                                  was: no check
unparsable    a tool's config doesn't parse → its record kept as it was (never saved as [])                    was: [] read as proof, released after the repair
shared write  a tool added to a file another tool recorded, its write fails → that (file, tool) taken back      was: kept recorded
              kept only while that tool's own records hold a resolved value there (Claude's write doesn't count)  was: kept by the file's write
formats       Cursor (mcpServers) + OpenCode (mcp) on .mcp.json, stale token under mcpServers → line kept       was: judged as OpenCode, released
              unclaimed servers under both keys → both noted                                                  was: the last format's only
              Cursor owns x under mcpServers, OpenCode's stale x under mcp → OpenCode's x unclaimed, line kept  was: claimed by Cursor's record
              notes settled on what every format sees (one format's empty key doesn't forget the file)        was: each format alone
uninstalled   OpenCode removed (.opencode gone), opencode.json with a token, its record and the sidecar lost,
              Claude's record intact → listed; doctor ✖                                                        was: released
re-included   dry run (`mcp list`, doctor) with `!/.mcp.json` in .gitignore → names the rule, withheld         was: pending, nothing said
              CodeBuddy, recorded as a writer of Claude's .mcp.json, uninstalled with its record lost → kept    was: released on Claude's presence
moved + key   a moved Cursor's file, OpenCode now owning x under `mcp`, Cursor's stale x under mcpServers → kept  guard: other rules held it
              in these fixtures too; the recorded-file path now also scopes ownership by key
HTTP team     local agent's install_mcp, project config, a header/env value → excluded before the write,
              recorded; tracked → nothing written, ack failed with the reason ("install the MCP server again")  was: written, committable
              doctor for an HTTP project scope → checks those configs                                          was: skipped
Copilot bare  bare server beside the mcpServers another tool added → seen by protection and cleanup           was: hidden
              Copilot (pull or local agent) writes that server again under mcpServers → bare copy removed     was: stale token left beside it
HTTP creds    local agent: an argument, any URL (a token can sit in its path), a command line → a credential  was: only headers and env
              doctor, HTTP team, no record of the tool, file listed in managed-mcp-files.json holding a server → ✖  was: skipped
              OpenCode's one-element command array with arguments in it → a credential                        was: missed
              doctor, HTTP team, one server's record lost while another's remains → judged by its entry      was: missed
              a config an older local agent wrote a credential into → listed and recorded on the next sync
              in that workspace (no command needed) and on a pull; the warning says a new session retries     was: never listed
              also after an uninstall_teamai that failed or kept the shared files                              was: skipped for any uninstall_teamai
Copilot bare  a member's own bare server of a name teamai writes under mcpServers → never removed             was: deleted
              removed only when it is exactly what teamai's record says it wrote (hash)
              a bare copy differing from mcpServers' entry of that name → line kept                          guard: another rule held it too
rebuild       a record marked unnoted after a failed note: claims scoped to the tools reading the same key    same rule as the other two sites (no separate test)
lost record   one tool's record lost, another's intact → its first record notes the file's unclaimed servers   was: not noted, released later
              same with that tool disabled (no record written) → listed while a server no record claims remains; doctor ✖  was: released
guards        member's own server, dry run, member removes the server, unusable managed-mcp-files.json, shallow clone,
              no history, git error, path outside the project → today's rules, no throw

Real CLI, before (R1-R4 34bb7c11, R5-R6 72799f5a, R8 16007ae2, R5b d19a6d6d, R9-R10 bf28224c, R11 16c1938f) and after, temporary HOME, Claude + Cursor, Bearer ${JIRA_TOKEN}:

R1 Cursor disabled, jira made a literal, token unset, pull   ?? .cursor/mcp.json ← token      →  excluded; .mcp.json (literal) released; doctor ✔
R2 custom Cursor path, team removes the mapping, pull        ?? .cursor/team-mcp.json ← token →  excluded; doctor names it; uninstall keeps it until deleted
R3 nested .cursor repo, linked worktree serverless, uninstall ⚠ block kept                    →  block removed
R4 manifest deleted, jira → docs, token unset, 3 pulls        ?? .mcp.json ← token             →  jira noted, line held, doctor ✔; member deletes jira → released
R5 origin/main pull writes to a custom path, team remaps it  ?? .cursor/team-mcp.json ← token →  first pull here: excluded, recorded; doctor ✔
R6 sidecar locked while a lost record is rebuilt             line released, ?? .mcp.json ← token → record marked unnoted, line held, noted after
R8 .cursor → tracked config/ (config/mcp.json tracked), pull  `git rm --cached .cursor/mcp.json` fails → names config/mcp.json; `git rm --cached` works
R8 .cursor → a directory outside any repository, pull        withheld on a git error          →  written, nothing listed
R5b old path's server renamed after the remap; doctor, pulls  history marked read, token file committable → doctor names it; pull lists and records it
R9  old file tracked on the first pull, then git rm --cached  ?? .cursor/team-mcp.json ← token → recorded tracked, then listed; doctor ✔
R10 Cursor + Claude on .mcp.json, Cursor moved, jira dropped  ?? .mcp.json ← token             →  line kept; released once the member deletes jira
R11 R10 with the first pull by a pre-#882 teamai              ?? .mcp.json ← token             →  doctor names it; pull lists and records it
R12 pre-#882 token at Cursor's built-in path; Cursor moved, jira dropped   (after only)  →  /.cursor/mcp.json listed and recorded; released once emptied
R13 pre-PR install, shared .mcp.json, CodeBuddy's record lost, jira dropped (after only)  →  /.mcp.json kept
R14 no managed-mcp.json, stale server dropped, token unset    (after only)  →  doctor ✖, then listed, noted, kept; doctor ✔
R15 CodeBuddy moved off .mcp.json, its token there, jira dropped (after only)  →  /.mcp.json listed and recorded for CodeBuddy; released once jira goes

Related Issues

Closes #882. Found while implementing #879 / #880.

Notes for Reviewers

Door: two-way. Only writes a marked block in a local, uncommitted file; uninstall removes it.

Blast Radius: project scope. Clones whose project MCP config holds a resolved ${VAR}; user-scope configs untouched.

  • Runs on every reconcile with a resolved value, so clones that got a token before this release are covered on their next pull. Pull only adds paths; the block goes on uninstall.
  • Design: an exclusion lives as long as the file needs it. A line this run added is taken back out if the file then gets no resolved value (malformed file, a member's own server under a team name). After a pull or teamai mcp remove leaves a file provably free of resolved values, its line is removed under the lock (the block with its last line); the proof uses the manifest as it stood before the command ran, so a pull that recreates a lost manifest can't open the block. Pull and mcp remove judge only the current worktree: while another worktree's copy of the file holds any server, the shared line stays (only a pull there, or uninstall, can judge it). A file listed before its write is listed again after it, so an exclusion a concurrent uninstall dropped is restored. A tracked file is named before an unwritable exclude file.
  • Design: excluded before it is written. A project MCP config whose new content would carry a resolved value is excluded first; the file is written only once git ignores it. If the exclusion can't be established (unwritable .git/info or exclude file, lock held past the wait, a git error, or a file git already tracks), teamai doesn't write that file: the earlier content and manifest entry stay, pull and mcp inject warn with the reason and fix, and mcp list and doctor show the server as withheld. A tracked file's fix is git rm --cached <file> and rotating the token.
  • Design: the block goes on proof, not on the absence of a failure report. Before removing a repo's block, uninstall inspects each path it protects. It removes a path's line only if that file is missing, parses and holds no MCP server, or holds none of the team's servers that need a resolved ${VAR} while managed-mcp.json still records what teamai wrote there (a lost manifest keeps the line, with a warning naming the file); the block goes with its last line. The check per file is carriesResolvedValue over the entries actually in the file, the predicate pull uses to add the path, applied to every entry rather than only the owned ones. The ownership manifest is not consulted, so a missing or unreadable managed-mcp.json, or ownership records dropped after a parse failure, cannot open the block. A file no detected tool reads, one that does not parse, or a team mcp.yaml that cannot be read keeps the block, with a warning naming the file. The member then removes the team's servers and the block by hand.
  • Design: protection covers what is on disk, not what this run delivered. The tool gate (disabledAgents/enabledAgents), autoApply: false, tool detection and a failed write stop delivery only. The protection pass runs in a finally after every non-dry reconcile and visits every tool's project MCP file. A failure inside it warns and points at teamai doctor, rather than going into pull's debug-level reconcile log.
  • Design: a git error is never read as safe. When check-ignore errors, tracking is decided by git ls-files --error-unmatch; a tracked file is withheld, and if ls-files can't answer either, nothing is written and git's error is the reason. A line this run added is rolled back only for a file this run did not write, so a later failure (the manifest write) keeps the line. mcp list reports withheld only for targets delivery would write the server to.
  • Design (earlier): a git error is never read as safe. check-ignore exit 0 means ignored and exit 1 means would commit. Anything else is "outside a repository" only when no .git exists above the file. Otherwise the path is excluded all the same, or teamai warns with git's error, and doctor fails the check.
  • Design: judged by the disk and the manifest, never by current config alone. One function, resolvedValueEvidence, decides for pull, doctor and uninstall. A teamai-owned entry still in the file counts when its server needs a resolved ${VAR}, when its server has left mcp.yaml (the definition that would prove it clean is gone), or when mcp.yaml cannot be read. Targets include the built-in location of a tool the team dropped from toolPaths or moved. Uninstall inspects every worktree of each repository owning a block, including a nested repository's linked worktrees. A file there that no tool reads keeps the block.
  • Design: a record of what a pull wrote. managed-mcp-files.json (next to managed-mcp.json) lists each config a pull writes a resolved value to, recorded after its exclusion and before the write, so a file left behind by a changed toolPaths mapping is still visited. When a lost managed-mcp.json is rebuilt, the servers in the file that the new record doesn't claim are noted there and keep the line until they leave the file or teamai owns them again. A file two tools share counts as recorded only while every tool the sidecar says wrote a resolved value there still has its record; with no sidecar entry, every tool the team maps there must have one. A tool's built-in location, once the team moves or drops the tool, is judged like an earlier-mapped file; when another tool maps it today, it holds while it contains a server that tool's records don't own. While a worktree has no managed-mcp.json, a config holding a server no record claims is kept out and that server noted. Records now carry resolved, so an entry unchanged since a pull wrote it keeps its line even after its definition turns literal. A config an older teamai wrote before this record existed is found once per worktree by reading every mcpProject in the team repo's history of teamai.yaml, plus the built-in defaults teamai has since changed; such a file is judged like a recorded one (it holds while it doesn't parse or holds any server), since today's records describe the new path, and doctor reads the same history until a pull has. One git tracks is recorded as tracked and judged once it isn't. A file a moved tool wrote keeps being judged for that tool while another tool maps it. A rebuilt record whose note fails to land is marked unnoted in the same manifest write, and its file keeps the line until a pull notes it. Missing or unreadable, the file reads as empty and the earlier rules apply.
  • Design: judged where the write lands. The writers replace a symlink at the file itself but follow its directories, so tracking, exclusion, release, doctor and uninstall all judge realFilePath(file): the real path of its closest existing directory, with the rest appended. .cursor/ linked to config/ gets /config/mcp.json in the exclude, and a tracked file there is named by the path that git rm --cached accepts. Reads keep the logical path.
  • Design: exclude updates are serialized. The worktrees of a repository share info/exclude. Each change takes the existing acquireLock helper from update.ts (retrying for about 2.5 s), re-reads the file, and writes it atomically. If the lock is still held after the wait, nothing is written: pull warns that the file isn't excluded yet and to run teamai pull again, uninstall keeps the block, and doctor's check fails until a pull succeeds.
  • Design: the resolved value is its own signal. Pull and doctor also count a file containing the resolved value (8+ characters) of a variable a team server needs there, so a lost manifest does not hide it. A member's own server under a team name matches only if it holds that same secret. Uninstall scans every variable set in the environment (paths and the login name excepted), which still finds a server since dropped from mcp.yaml. The kept-block warning names the file and why, such as the variable whose value matched, so a member can judge an ordinary value (NODE_ENV, a base URL). The scan stays fail-closed.

Known limits

  • A config an older teamai wrote under a mapping the team changed before this member's first pull on this version stays unfound only when the team repo's history no longer holds that mapping (history rewritten, a shallow clone without the revision) or the path is outside the project root.
  • A lost managed-mcp.json that an older teamai rebuilt noted no servers: a stale entry for a server since dropped from mcp.yaml, whose value is no longer set, then looks like the member's own, and its line can go. The same holds if managed-mcp-files.json is deleted after the rebuild.
  • A nested repository's linked worktree that holds any MCP server keeps the block: teamai has no manifest there to judge an entry.
  • It errs toward keeping a line: a noted server keeps it until it leaves the file or teamai pull --force reclaims it; an older record counts as resolved once today's definition no longer produces the entry; a file under a changed mapping (recorded, or found in the history of teamai.yaml) keeps it while it holds any server, the member's own included; so does a file another tool now maps, written for a tool the team moved, while it holds a server the tools mapping it didn't write; and an older record without resolved for a server with a tools: filter, in a file two tools map, reads as no longer produced until a pull rewrites it. So does a tool's built-in location once the team moves or drops the tool (a tool a custom toolPaths never listed counts as dropped), while it holds any server; and a shared file no pull on this version recorded, until both tools have a record (a Claude-only member with CodeBuddy on the same .mcp.json keeps the line until a pull records the file). CodeBuddy's .mcp.json, once the team moves or drops CodeBuddy, keeps it while it holds a server Claude's records don't own, one of the member's own included. And while managed-mcp.json has no record for a tool (the whole file lost, that tool's record lost, a new worktree's first pull, or teamai's first delivery to that tool), each pull (and doctor) treats its untracked config as unrecorded, and the pull notes each server no record claims; a tool this machine doesn't have counts when no installed tool maps its file or managed-mcp-files.json lists it as a writer there (CodeBuddy never installed beside Claude's .mcp.json doesn't); the config keeps its line until those servers leave the file or teamai pull --force reclaims them, a member's own server already there included. A note that cannot land (another command holds managed-mcp-files.json, an I/O error) keeps the line until a later pull writes it; if a pull empties a tool's record meanwhile, the record is dropped instead of kept empty, and the file keeps its line while it holds any server until the team delivers to that tool again.
  • For an HTTP-backed team, the local agent's install_mcp lists a project config before writing a server with any header, env value, argument or URL, or a command line with arguments: only a bare stdio command carries no credential (its payload carries literal values, so any counts). A file an older local agent wrote a credential into is listed on the local agent's next sync in that workspace, and on a pull there (the current workspace only; another is protected on its next session there). Only teamai uninstall takes such a line out: no pull or teamai mcp remove releases one for an HTTP team.
  • In a Copilot project config that also holds mcpServers, every other top-level object reads as a bare server; an object-valued setting there reads as a server no record claims, which keeps the line.
  • The value scan only finds values still in the environment when the command runs, and none under 8 characters.
  • A config under a dangling directory symlink is judged at the path through the link. No write can land there until the target exists, and the pull that writes then judges the target.
  • Doctor needs the team config (teamai.yaml) to enumerate targets. Without it, this check is skipped.
  • User-scope configs (~/.claude.json and similar) are out of scope. If $HOME is a git repository (dotfiles), such a file is not protected.
  • teamai mcp remove leaves the block: all worktrees share one exclude file.
  • If the teamai config is gone before uninstall, uninstall takes its no-config path and the block stays. It only ignores MCP config files, so it's harmless.
  • Merges cleanly with feat(env): team-declared secrets with member-local values (#875) #880 (trial merge: no conflicts, tsc and lint clean, unit suite passes).

…encent#882)

A project-scope MCP config that carries a resolved ${VAR} sat untracked
and unignored in the business repo, one `git add -A` from committing the
token. After the reconcile writes such a file and git would track it,
teamai lists its path in the clone's .git/info/exclude inside a marked
block (resolved via `git rev-parse --git-path`, so linked worktrees and
submodules work). The committed .gitignore is never touched; an ignored
path or a config with no resolved value adds nothing; dry runs write
nothing. Project-scope uninstall removes only teamai's block, and doctor
reports such a file git would still commit.

The hook sits after the appliers in reconcileMcpForConfig, outside
desiredMcpForTarget/applyJson/applyCodex, so it merges cleanly with Tencent#880.
…Tencent#882)

The plan now records whether the project's .git/info/exclude holds
teamai's MCP config block (gitExcludeBlock). It counts toward
isPlanEmpty, is listed in the summary and dry run, and gates the
removal, so a plan whose only teamai leftover is the block removes it
instead of reporting "Nothing to uninstall".
@jeff-r2026 jeff-r2026 self-assigned this Sep 28, 2026
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/uninstall.ts:1251 removes the ignore block even when MCP entries were not removed. For example, applyJson skips an invalid .mcp.json without throwing; uninstall then exposes the still-present plaintext token to git add -A. Remove exclusions only after confirming every relevant config was cleaned successfully.
  • [P2 non-blocking] src/doctor-delivery.ts:526 infers a resolved secret from server names alone. A user-owned server colliding with a team server name is skipped by reconciliation, but doctor incorrectly claims it contains TeamAI-resolved plaintext and recommends teamai pull, which cannot fix the warning. Compare the installed entry with the desired entry or ownership manifest.
  • [P3 nit] src/mcp-git-exclude.ts:84 appends a new block when an existing start marker has lost its end marker. A later uninstall pairs the original start with the newly appended end and deletes any user exclusions between them, contrary to the damaged-block safeguard.
  • [P3 nit] src/uninstall.ts:1252 only removes the block from the project root repository, although excludeFromGit explicitly supports MCP paths inside submodules/nested repositories. Such configurations leave TeamAI’s block behind after uninstall.

The PR description includes sufficient unit, e2e, and representative real-CLI testing.

…cent#882)

- uninstall keeps a repository's .git/info/exclude block while a config in
  it could not be parsed and still holds teamai servers, and warns
- uninstall finds and removes the block in nested repositories holding an
  MCP config, across every worktree
- the block opens at the last start marker, so an orphaned start never
  pairs with a later block's end and takes the member's lines
- doctor counts only servers the ownership manifest records, not a member's
  own server under a team name
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/uninstall.ts:1270 treats the absence of leftInPlace as proof that every protected config was cleaned. If managed-mcp.json is missing or unreadable, reconcileMcpForConfig returns early with no ownership records, leaving a resolved token in .mcp.json; uninstall then removes the exclude block, exposing that token to git add -A. Keep the block unless every protected path was positively inspected and cleaned.

The four earlier findings are resolved in the current diff. The PR description includes sufficient unit, e2e, and representative real-CLI testing.

…en clean (Tencent#882)

A missing or unreadable managed-mcp.json made the MCP cleanup return early
without reporting anything, so uninstall removed the block while .mcp.json
still held the resolved token.

Uninstall now inspects every path the block protects after the cleanup. The
block goes only when each one is missing, or parses and holds none of the
team's servers that need a resolved ${VAR}. Anything it cannot check keeps
the block, with a warning naming the file. This replaces the leftInPlace
report from the reconcile, which the check subsumes.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/mcp-reconcile.ts:568 skips excluded/disabled tools before applying the new Git protection. A project MCP file containing a plaintext token from an earlier pull remains on disk after its tool is disabled, but subsequent pulls never add it to .git/info/exclude; src/doctor-delivery.ts:527 skips the same target, so doctor also reports nothing. git add -A can commit the token. Existing owned entries must still be protected and diagnosed even when delivery is disabled.
  • [P2 non-blocking] src/mcp-git-exclude.ts:61 treats every git check-ignore failure as “Git would not track this file.” In a valid repository where Git returns 128—for example due to unsafe-repository configuration—the plaintext config is neither excluded nor warned about. Preserve an error state and warn rather than silently treating it as safe.
  • [P3 nit] src/uninstall.ts:539 proves cleanliness using only the team’s current MCP definitions. If a server was removed from mcp.yaml and its ownership manifest was also lost, its old plaintext entry is considered clean and the exclusion block is removed. This requires two failures, but exposes the remaining value.

The four earlier findings are resolved in the current diff. The PR description includes sufficient unit, e2e, and representative real-CLI testing.

…encent#882)

Pull, doctor and uninstall each skipped a case they had not inspected and
treated it as safe. Now:

- pull lists a config in .git/info/exclude whether or not it delivered to
  it this run: a disabled or undetected tool's file, a team with automatic
  delivery off, an unreadable mcp.yaml (any teamai entry counts), a failed
  write to another tool's config, and a lost ownership manifest (the
  resolved value found in the file)
- doctor checks the same files, including one that does not parse, and
  counts a git error as a failure
- git check-ignore failing inside a repository is no longer read as "not
  tracked": the path is excluded anyway, or teamai warns with git's error
- uninstall also keeps the block while a file contains the value (8+
  characters, not a path or the login name) of a variable still set in the
  environment, which finds a server since dropped from mcp.yaml
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/mcp-reconcile.ts:549 only recognizes owned entries whose server still exists in the current teamDefs. Concrete failure: an older pull writes a resolved jira token without an exclude block, the team later removes jira from mcp.yaml, and cleanup is skipped because .mcp.json is invalid or the tool is disabled. The token remains, but the protection pass adds no exclusion, so git add -A can commit it. src/doctor-delivery.ts:537 has the same false negative.
  • [P2 non-blocking] src/doctor-delivery.ts:519 returns without checking Git whenever the team MCP definitions cannot be resolved. With malformed mcp.yaml, a manifest-owned plaintext config that is tracked or no longer excluded receives only the generic parsing failure; doctor omits the specific credential-exposure warning even though pull conservatively treats that owned entry as sensitive.

The previously reported findings are resolved in the current diff. The PR description documents sufficient unit, e2e, and representative real-CLI testing.

…encent#882)

Pull, doctor and uninstall still decided "clean" from the current team
config in places. Now one function, resolvedValueEvidence, decides for all
three:

- a teamai-owned entry still in the file counts when its server has left
  mcp.yaml, as well as when it needs a resolved ${VAR} or mcp.yaml cannot
  be read (doctor no longer skips that case)
- targets include the built-in location of a tool the team dropped from
  toolPaths or moved
- doctor names a file two tools share once
- exclude updates take the existing acquireLock helper, re-read the file
  and write it atomically, so concurrent commands keep each other's paths
- uninstall inspects every worktree of each repository owning a block,
  including a nested repository's linked worktrees, and applies the
  manifest rule per worktree
- the kept-block warning names each file and why, such as the variable
  whose value matched
@github-actions

Copy link
Copy Markdown

Findings

  • [P2 non-blocking] src/uninstall.ts:553 cannot recognize configs in linked worktrees of a nested repository. findMcpGitExcludes expands the nested repository’s patterns across all its worktrees, but targets only covers worktrees of the outer project repository. Consequently, an existing but clean MCP config in a nested repo’s linked worktree is classified as “no tool teamai knows reads it,” so uninstall permanently retains an otherwise removable exclude block.
  • [P3 nit] src/mcp-git-exclude.ts:173 abandons serialization after 2.5 seconds and performs the read-modify-write without owning the lock. If another process remains paused or its filesystem operation exceeds that timeout, concurrent additions/removal can overwrite each other and lose a protected path. Waiting longer or failing safely would preserve the claimed locking guarantee.

The previously reported findings are resolved in the current diff. The PR description includes sufficient unit, e2e, and representative real-CLI testing.

SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 28, 2026
…esolved tokens out of git

# Conflicts:
#	src/doctor-delivery.ts
#	src/mcp-reconcile.ts
…s its lock (Tencent#882)

After the 2.5 s wait for the exclude file's lock, updateExclude wrote without
it, so two writers could drop each other's pattern and leave a plaintext MCP
config committable. It now writes nothing and reports 'locked': pull warns that
the file is not excluded yet and to run `teamai pull` again (doctor's exclude
check keeps reporting it meanwhile), and uninstall keeps the block and warns.
… free of teamai's servers (Tencent#882)

Uninstall judged a protected file clean from the current mcp.yaml, manifest
and resolvable values, so with the manifest lost, the server gone from
mcp.yaml and its value unset, a plaintext token looked like the member's own
server and the exclusion went. It now fails closed and works per entry: a
pattern goes only when its file is gone, holds no server, or holds none of
teamai's servers with managed-mcp.json still there to say what teamai wrote.
A kept entry is named with its file, why, and how to clean it by hand, since
a rerun of uninstall finds no config after a full uninstall.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/mcp-reconcile.ts:527 judges owned entries using only the current server definition. If an earlier pull wrote jira from ${TOKEN}, the tool is later disabled, and the team changes jira to a definition without variables, the stale plaintext token is no longer recognized; pull adds no exclusion and doctor reports nothing, so git add -A can commit it.
  • [P1 blocking] src/mcp-reconcile.ts:234 only revisits current mappings and built-in defaults. If a previous pull wrote a token to a custom mcpProject path and the team later changes or removes that mapping before the member upgrades, the old file is never inspected or excluded and can be committed.
  • [P1 blocking] src/uninstall.ts:542 treats the existence of any manifest file as proof for every MCP target. If one target’s ownership record is missing while another target still has records—or cleanup writes an empty {} manifest—a valid file containing a dropped server and an unset token is considered clean at src/uninstall.ts:574; uninstall removes its exclude line and exposes the plaintext value.
  • [P2 non-blocking] src/uninstall.ts:544 still cannot associate a nested repository’s linked-worktree files with an MCP target. A clean mcp.json in that linked worktree is classified as “no tool teamai knows reads it,” so uninstall retains the shared exclusion indefinitely. This previously reported finding remains unresolved.

The other earlier findings are resolved. The PR description documents sufficient unit, e2e, and representative real-CLI testing.

SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 29, 2026
…proof, skip unlocked writes

# Conflicts:
#	docs/usage-guide.md
#	docs/usage-guide.zh-CN.md
…lved value into it (Tencent#882)

Pull listed the file in .git/info/exclude only after writing the plaintext,
and a failed exclusion only warned, so the secret-bearing file stayed
eligible for git add -A. The exclusion now comes first; when it cannot be
established (exclude file or .git/info not writable, lock held past the
wait, file already tracked, git error) the file is left as it was and the
warning names the reason and the fix.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/mcp-reconcile.ts:525 treats an owned server as safe when its current definition keeps the same name but removes ${VAR}. If an earlier pull wrote a token and the tool is subsequently disabled, dropped and needing are both false and the value scan has no referenced variable to inspect; pull and doctor skip the file, allowing git add -A to commit the stale token.
  • [P1 blocking] src/mcp-reconcile.ts:234 still enumerates only current mappings and built-in defaults. If an earlier pull wrote a token to a custom mcpProject path and the team later changes or removes that mapping, the old file is never inspected or excluded, so it remains commit-able.
  • [P1 blocking] src/uninstall.ts:542 considers the existence of any manifest file proof for every target in that worktree. With an empty {} manifest—or records only for another target—a config containing a dropped server and an unset token reaches src/uninstall.ts:574 as clean; uninstall removes its exclude entry and exposes the plaintext value.
  • [P2 non-blocking] docs/usage-guide.md:1163 documents the runtime behavior, but the affected agent-facing MCP and uninstall guidance under skill-data/setup/references/ remains unchanged, contrary to the repository rule requiring behavior changes to update affected skills.

The earlier nested-repository linked-worktree issue and the other previously resolved findings remain fixed. The PR description documents sufficient unit, e2e, and representative real-CLI testing.

SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 29, 2026
…ed value

Tencent#882's pre-write exclusion is now the single gate for a project MCP config
git would commit. Tencent#880's separate tracked-file check (gitTracks in
buildDesiredMcpContext, `withheld` in desiredMcpForTarget, its warning,
mcp list lines and doctor note) is removed: a tracked file is one more way
the exclusion fails, reported once with `git rm --cached <file>` and rotate.
A dry run (mcp list, doctor) now also names a tracked file before any pull
has listed it, mcp list reports a withheld file with an entry already
installed, and doctor reports withheld servers without the pull --force text.
A tracked file now gets no resolved value, declared secret or not.
…ready installed (Tencent#882)

Backports Tencent#880's merge 0d9f7fa: a dry run (doctor, mcp list) names a
tracked file before any pull has listed it, mcp list reports withheld for a
server an earlier pull installed, and doctor's withheld note carries the
exclusion's own fix instead of the pull --force advice.
…clude (Tencent#882)

A tracked file needs `git rm --cached` whatever else is wrong, so
ensureExcludedFromGit checks gitTracks before the writability check, on a
pull and a dry run alike, and lists nothing for it.
…olds no resolved value (Tencent#882)

A pull that lists a config in .git/info/exclude and then writes no value
into it (it does not parse, a member's server holds the team's name, the
write fails) removes the line it added. After a pull or `teamai mcp
remove`, a line whose configs are proven clean in every worktree, by the
proof uninstall uses (moved to mcp-reconcile.ts), is removed under the
lock; one not proven clean stays. A config listed before its write is
listed again after it, so a concurrent uninstall that dropped the line
between the check and the write does not leave the value unprotected.
…re the pull rewrote it (Tencent#882)

A pull whose manifest was lost before it ran recreates managed-mcp.json
while reconciling, so the clean-file proof read the new record and took
the exclude line out of a file still holding a teamai server that left
mcp.yaml with its variable unset. The proof now uses this worktree's
manifest as read before the reconcile.
A line this pull added and took back out, because it wrote no resolved
value into the file, was reported as removed although the member never
saw it added. Only removing a line an earlier run added stays at info.
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/mcp-reconcile.ts:535 treats an owned entry as safe when its same-named definition stops referencing ${VAR}. If a disabled tool retains the old plaintext token, neither dropped, needing, nor the current-definition value scan detects it, so the file remains commit-able.
  • [P1 blocking] src/mcp-reconcile.ts:245 visits only current mappings and built-in defaults. If a team replaces or removes a custom mcpProject path, the previously written file is never inspected or excluded, allowing its resolved token to be committed.
  • [P1 blocking] src/mcp-git-exclude.ts:180 checks whether the file is tracked only when check-ignore returns would-commit. If check-ignore errors while rev-parse still locates the repository, a tracked file gets an exclude line and the second error is accepted as success at src/mcp-git-exclude.ts:215; reconciliation can then write plaintext into a file that Git already tracks.
  • [P2 non-blocking] src/mcp-reconcile.ts:593 still cannot associate a nested repository’s linked-worktree files—added by src/mcp-git-exclude.ts:283—with MCP targets. Even a missing-token, clean config is classified as unreadable by any known tool, so uninstall cannot remove its shared exclusion.
  • [P2 non-blocking] src/mcp-cmd.ts:79 reports a server as “withheld” for every target requiring resolution without applying its tools, transport, policy, exclusion, or requirements filters. For example, a Cursor-only server can incorrectly be reported as withheld from Claude.
  • [P2 non-blocking] The runtime behavior is documented only in docs/usage-guide.*; the affected agent-facing MCP/uninstall guidance under skill-data/setup/references/ remains unchanged, contrary to the repository rule requiring behavior changes to update affected skills.

The other previously fixed findings remain resolved. The PR description documents sufficient unit, e2e, and representative real-CLI testing.

SaulMoro added a commit to SaulMoro/teamai-cli that referenced this pull request Sep 29, 2026
…holds a server (Tencent#882)

A pull or `teamai mcp remove` judged every linked worktree's MCP config
with today's definitions and values. Once a server's ${VAR} became a
literal, and the value was no longer set, a pull in worktree A took
worktree B's stale token-bearing entry for clean and removed the shared
/.mcp.json line, so `git add -A` in B staged the token.

These commands now release a line only when the current worktree's file
passes the full proof and every other worktree's file is missing or holds
no MCP server. `teamai uninstall` keeps its full proof in each worktree.
…onfig, and pin an uninstalled tool's leftover config (Tencent#882)
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/mcp-reconcile.ts:1122 still trusts a missing record for an undetected tool whenever another tool’s manifest record remains. Concrete failure: OpenCode previously wrote a token, its manifest key and sidecar are lost, OpenCode is uninstalled, Claude’s manifest key remains, and the server is later dropped with its variable unset. Neither pull nor src/doctor-delivery.ts:603 treats the OpenCode entry as unclaimed, so the exclusion is not restored and git add -A can commit the token. The previously reported undetected-tool finding remains unresolved for partially intact manifests.
  • [P2 non-blocking] src/mcp-git-exclude.ts:240 returns pending on a dry run before checking whether a higher-precedence negated ignore rule would override the proposed exclusion. With !/.mcp.json in .gitignore and no existing TeamAI line, mcp list and the delivery doctor check omit the “withheld” reason, although the subsequent pull adds the ineffective line and refuses the write. Simulate or verify the post-add ignore result during dry runs.

The other previously reported findings appear resolved. The PR description documents sufficient unit, e2e, and representative real-CLI testing.

…d tool maps its file, and name a re-including .gitignore rule on a dry run (Tencent#882)
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/mcp-reconcile.ts:1138 merges claimed server names across every tool sharing a file. If Cursor and OpenCode use different keys in the same .mcp.json, OpenCode’s manifest record is missing/disabled, and Cursor owns a same-named server whose definition is now literal, Cursor’s record suppresses OpenCode’s unclaimed entry. Pull and doctor then fail to restore a missing exclusion, leaving OpenCode’s stale plaintext token commit-able. Ownership must remain scoped to each tool/format.
  • [P1 blocking] src/mcp-resolved-files.ts:220 filters the file-wide unverified list independently using each target format’s server list. For a shared Cursor/OpenCode file, the Cursor observation removes names visible only under OpenCode’s mcp key (and vice versa). If that tool is later undetected, the note is not recreated; after the exclude block is cleared, later pulls and doctor can miss the stale token. Settle unverified only after aggregating every format for the file.

The previously reported findings appear resolved. The PR description includes sufficient unit, e2e, and representative real-CLI testing.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/mcp-reconcile.ts:304 treats an undetected tool’s missing manifest record as safe whenever any installed tool maps the same file, even when managed-mcp-files.json records that the undetected tool previously wrote resolved values there. For example, CodeBuddy writes a token to the .mcp.json it shares with Claude, is later uninstalled, loses only its manifest key, and its server is dropped with the variable unset. Claude’s presence makes unrecordedMcpTool return false, so pull and src/doctor-delivery.ts:604 never examine the stale unclaimed entry or restore a missing exclusion, allowing git add -A to commit the token. A recorded writer must remain suspect even when another installed tool maps the file.

The previously reported findings appear resolved. The PR description documents sufficient unit, e2e, and representative real-CLI testing.

…ts as a writer, though an installed tool maps the file (Tencent#882)
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/mcp-reconcile.ts:786 treats a server name owned by any current mapper as ownership proof for a historical target, even when the tools use different JSON keys. For example, Cursor previously writes tokenized x under mcpServers, then moves elsewhere while OpenCode maps the old file and owns a literal x under mcp; recordedMcpFileEvidence considers Cursor’s stale entry clean and pull, doctor, or uninstall can remove/miss the exclusion, allowing git add -A to commit the token. Filter mappedBy ownership using sameServerKey, as the current-target path does.

The previously reported findings appear resolved. The PR description documents sufficient unit, e2e, and representative real-CLI testing.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/mcp-reconcile.ts:1124 skips protection entirely for HTTP-backed teams, although install_mcp writes project-scoped credentials such as bearer tokens directly into MCP config files. Those files remain visible to git add -A, and src/doctor-delivery.ts:559 suppresses the corresponding warning. The local-agent install path must establish the exclusion before writing, or HTTP projects must participate in this protection pass.
  • [P1 blocking] src/mcp-reconcile.ts:613 can miss a stale bare Copilot project entry after another tool writes a nested mcpServers section into the same file. readJsonDoc(..., allowBare: true) switches entirely to mcpServers once that key exists, so the original top-level Copilot server—and its plaintext token—is invisible to reconciliation and cleanup. A moved Copilot plus Claude sharing that path can therefore cause the exclusion to be removed while the token remains.

The previously reported findings appear resolved. The PR description documents sufficient unit, e2e, and representative real-CLI testing.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/mcp-reconcile.ts:1584 leaves a Copilot server’s old bare entry untouched when mcpServers later exists and the server is updated. For example, Copilot first writes a tokenized top-level jira; Claude then adds mcpServers; when jira becomes literal, Copilot writes the new value under mcpServers.jira but leaves the plaintext top-level jira. Because installedMcpEntries merges both maps by name and the nested entry wins, the protection pass considers the file clean and removes its exclusion, exposing the stale token to git add -A. Updating an owned Copilot entry must remove or replace the same-named doc.beside entry.

The previously reported findings appear resolved. The PR description documents sufficient unit, e2e, and representative real-CLI testing.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/mcp-git-exclude.ts:46 recognizes only headers, env, and environment as local-agent credentials. A project-scoped install_mcp command containing a token in args or an authenticated url therefore writes plaintext without adding an exclusion; git add -A can commit it. Inspect every credential-capable MCP field, as the Git-backed ${VAR} path already does.
  • [P1 blocking] src/mcp-reconcile.ts:1487 merges claimed server names across different JSON keys. Concrete failure: Cursor owns x under mcpServers, OpenCode’s record is missing while its stale tokenized x remains under mcp, and the sidecar lock prevents the protection pass from recording it. OpenCode’s rebuilt record is not marked unnoted; the next pull treats the stale entry as clean and removes its exclusion. Scope claimed with sameServerKey.

The previously reported Copilot bare-entry finding is resolved. The PR description documents sufficient unit, e2e, and representative real-CLI testing.

…and command line as credentials, and scope the rebuild's claims by key (Tencent#882)
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/mcp-git-exclude.ts:55 treats only URL credentials in the user-info or query string as sensitive. An HTTP install_mcp payload such as https://example.com/mcp/<token> has a plaintext credential in its path, but carriesLocalAgentCredential returns false, so the project config is written without an exclusion and git add -A can commit it.
  • [P1 blocking] src/doctor-delivery.ts:611 checks HTTP-team configs only through managed-mcp.json records. If that manifest is missing or unreadable, an existing project config containing a credential is ignored even when managed-mcp-files.json records the file; if its exclusion is also absent, doctor reports no failure and the credential remains commit-able. Inspect recorded/current files conservatively when ownership records are unavailable.
  • [P3 nit] src/local-agent.ts:2976 and src/doctor-delivery.ts:616 still describe protected data only as “header or env values,” although arguments, command lines, and authenticated URLs now trigger protection too.

The previously reported findings appear resolved. The PR description documents sufficient unit, e2e, and representative real-CLI testing.

… have doctor judge a recorded HTTP-team file with no record (Tencent#882)
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/mcp-git-exclude.ts:55 misses OpenCode command lines because OpenCode renders command: "server --token secret" as the one-element array ["server --token secret"]. The array-length check returns false, so a project install writes the plaintext token without an exclusion and git add -A can commit it.
  • [P1 blocking] src/mcp-reconcile.ts:1597 assumes every same-named bare Copilot entry is an old TeamAI copy. If a member owns a top-level jira and another tool has added mcpServers, the first pull writes TeamAI’s nested jira; the next pull or removal deletes the member’s bare entry. src/local-agent.ts:2949 and src/local-agent.ts:3047 have the same destructive behavior.
  • [P1 blocking] src/mcp-reconcile.ts:606 collapses same-named bare and nested Copilot entries, with the nested value winning. An older pull can leave a tokenized bare entry beside a newer literal nested entry; when Copilot is disabled and the token is unset, protection sees only the literal entry and can remove the exclusion, exposing the stale token to git add -A.
  • [P1 blocking] src/doctor-delivery.ts:615 handles a completely missing HTTP-team tool record but not a partially lost record. If a file contains credential server A and bare server B, A’s manifest record is lost while B’s remains, and the exclusion is absent, doctor checks only B’s resolved: false record and reports no exposure although A remains commit-able.

The other previously reported findings appear resolved. The PR description includes sufficient unit, e2e, and representative real-CLI testing.

…e Copilot copy, hold a shadowed one, and judge a partly lost HTTP record by its entries (Tencent#882)
@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/mcp-reconcile.ts:1153 skips the protection pass for HTTP-backed teams, while src/local-agent.ts:2966 adds an exclusion only during a fresh install_mcp command. Concrete failure: a pre-upgrade local-agent installation already left a credential in a project MCP config, then routine sync sends no new install command; upgrading or running teamai pull never excludes the existing file, so git add -A can commit the credential. Doctor only reports the exposure. Add an HTTP migration/protection pass over existing recorded/current project configs.

The previously reported findings appear resolved. The PR description documents sufficient unit, e2e, and representative real-CLI testing.

@github-actions

Copy link
Copy Markdown

Findings

  • [P1 blocking] src/local-agent.ts:3275 sets uninstalling merely because the response contains an uninstall_teamai command, so src/local-agent.ts:3293 skips credential protection even if that command fails or performs a non-last-agent uninstall that intentionally leaves shared MCP files intact. A pre-upgrade, unexcluded project MCP credential therefore remains visible to git add -A after this sync. Skip protection only after a successful uninstall that actually removes shared resources.

The previously reported findings appear resolved. The PR description documents sufficient unit, e2e, and representative real-CLI testing.

…amai too: a failed or partial one leaves what to keep out of git (Tencent#882)
@github-actions

Copy link
Copy Markdown

Review Result

  • No findings in the current diff.
  • The previously reported uninstall_teamai protection issue is resolved: workspace MCP protection now runs after successful, failed, or partial uninstall commands.
  • The PR description documents sufficient unit, e2e, and representative real-CLI testing.

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.

[feat] keep project MCP configs with resolved tokens out of git

2 participants