Repository navigation
feat(copilot): sync MCP servers - #628
Merged
Merged
Conversation
|
Findings
Testing
|
jeff-r2026
self-requested a review
September 18, 2026 13:01
ousamabenyounes
force-pushed
the
fix/issue-610
branch
from
September 19, 2026 11:23
cfd7dd7 to
7547b14
Compare
The PR description includes both a test plan and a real authenticated CLI E2E record, so it satisfies the review-description testing requirement. |
|
Contributor
Author
|
Addressed the remaining Copilot local-agent review findings in ca6b896: user MCP operations now honor COPILOT_HOME, and project operations preserve bare .github/mcp.json maps with unmanaged-collision protection. RED: the three focused regressions failed for the intended pre-fix reasons. GREEN: 17/17 focused tests, 72/72 related tests, the full ignored run-ci.sh suite, 100% changed executable line coverage (69/69), the 12-case real built-CLI agent/provider matrix, and authenticated Copilot CLI MCP E2E. The PR body now contains the complete verification record. No merge performed. |
jeff-r2026
pushed a commit
that referenced
this pull request
Oct 23, 2026
#1265) * refactor(locate): return Location<R> through LOCATORS with place kinds (#1221) locate now answers with a channel ('files', 'run-time', 'none', 'unknown') and places that say what kind they are: a directory, a blocks file, or a map inside a shared config file. A locator per resource sits in LOCATORS, so a resource without one fails tsc, and each resource keeps its own result type. Skills is still the only resource. The helpers serve every place kind: writable (was writableDir) and ownOf replace the 12 writableDir calls and 4 reads of .own; writesOf, cleanupOf and unreadWrites also handle files, map entries and a read hidden by an earlier file of a first-existing lookup ('shadowed'). No behavior change: A2's skills matrix and locate tests pass with only the accessor adapted to places. * refactor(locate): parse gitExclude, self mode and workspace children into Install (#1221) installOf now also reads sharing.gitExclude, whether the business repo is the team repo, and whether the workspace has child checkouts, once, so the rules, blocks and MCP locators get them from Install instead of each caller. Host gains realpath and readText, the two reads the key Claude Code files a checkout under needs (#915). Nothing reads the new fields yet. * refactor(locate): move Codex project trust out of doctor into its own module (#1221) codex-project-trust.ts now holds Codex's verdict on a project and the fix doctor prints, taking the ReadGate locate declares, so the agents and MCP checks can share one reading. Doctor's MCP trust check builds the same gate and prints the same lines. * refactor(locate): rename Home.moves.partial to settingsAtDefault (#1221) The flag now says what still goes to the default home when OpenCode, Pi or Muse moves its home: teamai's hooks and models switch settings, until B4 moves them. * refactor(locate): record where each tool takes a resource under places, not skillDirs (#1221) skill-dirs.ts becomes places.ts: lastPullByWorkspace[key].places holds, per resource, the places each tool took it from at the last full pull. Skills is the only resource recorded; rules, blocks and agents join once their writers go through locate. The fast path compares every recorded resource (placesUnmoved). A record from an earlier release reads its skillDirs as places.skills, and pull never writes skillDirs again. * feat(tools): state every tool's rules, blocks, agents and MCP channels in the descriptor (#1221) Each built-in tool now names, per scope, how it takes rules, instruction blocks, agents and MCP servers: the adapter with the paths it reads, a run-time channel with the probe that says it is installed, 'none', or 'unknown'. Leaving a resource or a scope out fails tsc (tools.type-test.ts). The rows hold the #1221 design, including WorkBuddy's MCP lookup (D-e), ZCode's mcp.servers (D-c) and no MCP channel for the internal builds (D-f); no writer reads them yet. The per-tool tables that decided the same facts are now views of the descriptor: the rule formats' tool keys, the session-hook and extension rule tools, detectMcpFormat's sets and the agent tool list. A test pins them to the tables they replaced. The internal builds keep their MCP format until the MCP writers move to locate. * feat(tools): locate rules, blocks, agents and MCP from the descriptor (#1221) Every configured or declared tool path now becomes absolute in rootsOf, and every resource takes one write gate per tool (gatesOf), so the blocks file, OpenCode's activation and the MCP file resolve once and move with a home. No caller uses the new locators yet; their tables run through one shared matrix runner, which the skills matrix now uses too. * refactor(agents): read the agent tools from the descriptor's AGENT_TOOLS, not ALL_SUPPORTED_TOOLS (#1221) ALL_SUPPORTED_TOOLS was a copy of AGENT_TOOLS read through casts. Its readers narrow with isAgentToolId instead, and the copy is gone. * test(locate): state where every tool takes agents, in both scopes (#1221) One row per tool and scope, plus the moved homes: OpenCode under XDG_CONFIG_HOME, OPENCODE_CONFIG_DIR and a recorded home, Muse under XDG_CONFIG_HOME, Copilot under COPILOT_HOME, and Claude's toolRoots. * test(locate): state every tool's rules and blocks in the matrix (#1221) Every tool in both scopes, each row citing its source. The dsh workspace cells follow #1223: dsh loads teamai's hook config, so its SessionStart hook carries the workspace rules and blocks. * feat(mcp): write each tool's servers where locate says it reads them (#1221) resolveMcpTargets now takes every target from locate(tool, 'mcp'): WorkBuddy's user servers go to the first existing of its lookup order, its project servers to CodeBuddy's local scope under sharing.gitExclude, and OpenCode, Muse and Pi follow a moved home (XDG_CONFIG_HOME, OPENCODE_CONFIG_DIR, PI_CODING_AGENT_DIR). The local-scope tools, the file readers of claude's .mcp.json and OpenCode's plugin file come from the descriptor instead of tables here. Copilot's MCP file still says it is installed (#628). * feat(local-agent): install MCP servers where locate says the tool reads them (#1221) install_mcp and uninstall_mcp take the tool's MCP file from locate(tool, 'mcp'), as a pull does: WorkBuddy's user servers go to the first of its files that exists, and OpenCode, Muse and Pi follow a moved home. USER_MCP_LOOKUP and userMcpFile go; doctor's read-file check names the target's own lookup files. * fix(rules): write rules where locate puts them, so they follow a moved home (#1221) Every rules directory pull, push, the pre-push sync, the built-in rules, recall disable, the Cursor user-rules hook, doctor and the local agent's report read now comes from locate(tool, 'rules'). OpenCode under XDG_CONFIG_HOME or OPENCODE_CONFIG_DIR gets its rules and their `instructions` glob in the config dir it reads, and the glob goes to the activation file locate names instead of the MCP field. Muse under XDG_CONFIG_HOME and Pi under PI_CODING_AGENT_DIR get the team-rules block in the AGENTS.md they read. The rules locator keeps honoring a team toolPaths entry written before a tool's rules moved (#938, #946): it names a rules directory the tool never reads, so locate returns that directory with no reads, pull still writes there and doctor still fails it. The inline team-rules block comes from the blocks location, so such an entry keeps its block too. * feat(agents): deliver agents where locate says each tool takes them (#1221) agentToolDirs, which the pull writer, doctor and the model holds read, asks locate. OpenCode and Muse agents now follow a home that XDG_CONFIG_HOME, OPENCODE_CONFIG_DIR or the tool-homes record moves, as their skills already did. * feat(agents): deploy and remove the built-in agents where locate says (#1221) deployBuiltinAgents and recall off ask locate, so the recall agent follows a moved OpenCode or Muse home too. deployBuiltinAgents now takes the local config: every caller passes one, and the test that called it without one passes the member's config instead. * feat(locate): retire the agents directory a team maps for a tool teamai renders none for (#1221) An earlier release copied team .md agents into any configured agents directory, so teamai remove, the tombstone pass and uninstall still clear its copies there. * feat(agents): push and remove read the agents directory locate names (#1221) The push scan and teamai remove ask locate for each tool's agents directory, so they see the copies in a moved OpenCode or Muse home. A custom tool, for which teamai renders no agents, is no longer scanned or cleaned. * feat(agents): pull's tombstone pass and target set ask locate for agents (#1221) * feat(workflow): load a step's agent from where locate says pull wrote it (#1221) runLocalDriver's toolPaths are now the team's as teamai.yaml states them, and loadAgent asks locate, so a user-scope agent in a moved OpenCode home loads. * feat(uninstall): find agents in every directory locate names for them (#1221) * feat(init): seed the agents directory locate names (#1221) A custom tool gets no agents directory: teamai renders no agents for it. * feat(agents): record where each tool took agents, and name a directory it left (#1221) places.agents joins the record the fast path compares, so a home that moves at one team revision syncs agents in full, and the first pull after it names the old directory once. A tool that did not take every agent (a member's own file, a held model, a failed write or built-in) keeps its old directory on record until it does. The record of an earlier release names no agents places, so its first pull syncs in full: the skillDirs upgrade test now expects that, and the fast-path fixtures record agents places as this CLI does. * feat(doctor): say where each tool takes agents, and whether Codex trusts the project that holds them (#1221) Doctor names a tool whose agents moved since the last pull, notes a tool whose agents follow a moved home while its other files stay in the default one, and checks Codex trust for a team that ships agents and no MCP servers. * test(agents): drop the agents path assertions the locate table states (#1221) The agents rows of locate-agents.test.ts state each of these paths with its source: OMP's user agents, Qoder CN's user agents, WorkBuddy's default, and Copilot's project directory. The whole default toolPaths literals and the toolRoots helper outputs stay, since they pin every other key too. * docs(reference): agents follow OpenCode's and Muse's moved homes (#1221) * feat(mcp): note each server's file in its ownership record (#1221) Every MCP writer now records the file it wrote a server to: pull, which `mcp inject` and `mcp remove` run, and the local agent. A record whose file the tool no longer reads splits three ways: - in one of teamai's own relocations (a lookup file, the mapped or alternate file, an earlier release's default): moved out, as before; - still holding the server elsewhere, after a home move or a remap: kept, and named once, as skills are; - anywhere else: taken as the current file's. A record an older teamai wrote notes no file. It reads as the mapped file for a lookup target, the default home for OpenCode, Muse and Pi, and the target's file otherwise; one whose entry an earlier release's default holds unchanged is moved out of that file. locate marks the built-in path of a remapped tool 'builtin', apart from an earlier release's 'default': a remap leaves the old file as it is. * fix(hooks): count a session-start channel ready only once its hook is installed (#1221) instructionHookChannel answered ready for every tool it did not know, so a ZCode, DeepSeek Harness or Muse Code channel with no teamai hook counted as delivering. channelReadiness takes the probe the tool's run-time channel names and checks it: the SessionStart hook in the hooks file for Codex and Muse Code, that hook plus hooks.enabled for ZCode, the cordis.patch.yml row and the bridge config for DeepSeek Harness. A new probe fails the type check until it has a check. * feat(uninstall): find each tool's MCP places through locate (#1221) Uninstall takes the servers of a partition that is no project out of each local scope where locate places it, so WorkBuddy's in .codebuddy.json go too, and CODEBUDDY_CONFIG_DIR is read through the host. The files whose git exclusions it removes are every place cleanupOf names for a tool: its file, OpenCode's plugin file, the other files of a lookup and the retired ones. * fix(rules): give Muse Code, dsh and ZCode a project's instruction blocks at session start (#1221) In a project these three read no file teamai could write the culture, claudemd and recall blocks to, and their session-start hook added only the rules, so they got none of the blocks. Whether a tool takes a scope's blocks from a hook now comes from its run-time blocks channel in the descriptor: the hook adds the blocks before the rules, the HTTP agent's prompts ride the same hook and are written to no file, and the hook's limit comes from the descriptor. project-hook-rules.test.ts pinned "rules only, no instruction blocks" for the three: that was #945's requirement, and #1221 replaces it, so the test asserts the blocks now. A run-time channel is probed in the member's home, where the hook or extension lives; the instruction-targets shape test creates ~/.pi for Pi as it already did ~/.omp. * refactor(mcp): read and write a server map under a key path (#1221) readJsonDoc and writeJsonDoc take the keys from a file's top to its server map, keeping every other key on the way, and a target carries the key locate names when it is not the format's own. No target has one yet; ZCode's `mcp.servers` is next. The local agent writes under the key of the place it writes. * fix(mcp): deliver ZCode servers through its native config (#1221) ZCode 3.16.2 reads mcp.servers in its user and project config. Move owned entries out of the former shared file, keep unrelated keys, and judge nested server maps for git exclusion. The outside-home path assertion changes because MCP now lives in the ZCode home. * fix(rules): write instruction blocks where locate puts them (#1221) Pull, the retire pass, uninstall, doctor, the OpenCode registration and the local agent now take each tool's blocks file from locate(tool, 'blocks'). USER_TARGETS, PROJECT_TARGETS, GIT_EXCLUDED_PROJECT_TARGETS, entryFor, instructionTargetFile, instructionTargetPath, instructionTargetAt, retiredInstructionFiles, userRulesFile, readsTeamRulesFromFile and isInstructionToolInstalled are gone; block text and planning stay in instruction-targets.ts. Behavior that moves with it: - Muse, OpenCode and Pi get their user blocks in the home they read (XDG_CONFIG_HOME, OPENCODE_CONFIG_DIR, PI_CODING_AGENT_DIR), and OpenCode's own AGENTS.md is looked for there before the Claude fallback is used. - OpenCode's `instructions` entry goes to the file locate names as the blocks activation, not the one the MCP field names. - A tool has the teamai-recall subagent only where locate gives it an agents directory, so a custom tool with an `agents` path gets the direct recall block. - blocksFileOf takes a fixed `dir` for CodeBuddy's and WorkBuddy's project file: CodeBuddy's engine reads .codebuddy/rules whatever the team maps, so both keep sharing one copy there. - One install gate per tool: WorkBuddy is installed where .workbuddy is. The #945 shape test counted an entry with only a bare `claudemd` as installed; it now creates .workbuddy, since that requirement is gone. * fix(mcp): use WorkBuddy project lookup and retire the old file (#1221) WorkBuddy uses the same project lookup as CodeBuddy. Update the old default-path assertion to the observed .mcp.json requirement, and clean the previous .workbuddy/mcp.json entries by ownership hash in both tree and local scope. * feat(doctor): report MCP read locations and unknown channels (#1221) * fix(local-agent): preserve MCP homes and migrate retired config keys (#1221) * test(mcp): cover tool locations in the shared matrix (#1221) * docs(mcp): explain native configs and moved homes (#1221) * fix(mcp): retain recorded config addresses during cleanup (#1221) * fix(local-agent): name old MCP configs for TOML installs (#1221) * fix(doctor): mark ZCode fallback loss as a warning (#1221) * feat(rules): record where each tool took rules and blocks, and name a place it left (#1221) A moved home has to redeliver rules and instruction blocks on an unchanged revision, and a copy the member kept has to leave the old place on record. The ledger's rules set is what makes that incomplete. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(doctor): say where each tool takes rules and instruction blocks (#1221) A moved home and a directory the tool does not read were visible for skills only. Rules and blocks get the same lines. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(rules): take rules and CLAUDE.md paths from locate (#1221) Uninstall scanned a rules directory it built with path.join, and the legacy rules-section strip did the same for CLAUDE.md. Both now ask locate. A retired claudemd stays in the strip. A rules directory stays in the uninstall scan. The guard is the skills twin, and it balances parentheses. The skills regex stops at the first nested call, so it missed path.join(resolveToolBaseDir(tool, localConfig), toolPaths[tool]?.rules ?? ''). Red, before the move, from npx vitest run src/__tests__/locate.test.ts -t "rules directory or claudemd": AssertionError: expected [ …(3) ] to deeply equal [] + [ + "resources/rules.ts: path.join(baseDir, toolPath.claudemd)", + "uninstall.ts: path.join(baseDir, toolPath.rules)", + "uninstall.ts: path.join(resolveToolBaseDir(tool, localConfig), toolPaths[tool]?.rules ?? '')", + ] The allow-list is empty. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(rules): take OpenCode's retired project config from the rules location (#1221) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(rules): remove tombstoned copies from a rules directory teamai no longer writes (#1221) Co-authored-by: Cursor <cursoragent@cursor.com> * docs(reference): state where rules and instruction blocks follow a moved home (#1221) Co-authored-by: Cursor <cursoragent@cursor.com> * test(pull): give fast-path fixtures the rules and instruction blocks a pull records (#1221) Co-authored-by: Cursor <cursoragent@cursor.com> * test(pull): expect the instruction-block notice when an OpenCode agent home moves (#1221) A moved OpenCode home also leaves teamai-context.md behind. The agent place tests still required that line to stay silent. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ci): keep the Codex team-hook command on bare bash (#1221) Merging main failed Windows CI because the team-hook dispatcher still substituted a Git Bash path into command, while main pins bare bash on every platform and carries the Windows form in commandWindows. The same merge left the doctor fixture without OpenCode's rendered mcp entry, and the WorkBuddy teamai-only case expecting the retired project file. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(local-agent): resolve a local-scope MCP record without a file in that scope (#1221) A record an earlier release wrote for Claude's or CodeBuddy's local scope notes no file. It resolved against the project's file instead, so with sharing.gitExclude on, install_mcp refused to update the server as not managed by teamai. Pull never adds the file in HTTP mode, so the record did not heal. * fix(rules): retire a mapped claudemd for a tool with no blocks channel (#1221) An earlier release wrote the instruction blocks into a claudemd the team mapped for JoyCode, Kiro, Qoder or Trae. Pull no longer writes there, so it now strips teamai's blocks from that file and uninstall finds them, as the rules and agents locators already do for a configured directory. team-culture.md named that claudemd as the workspace channel for ZCode, DeepSeek Harness, JoyCode and Muse Code; the first three now get the blocks from the session-start hook, and JoyCode gets none. * fix(rules): list OpenCode's user instruction file whatever the team maps MCP to (#1221) registerOpencodeContext took the config from the MCP mapping, so a team mapping no opencode mcp got no instructions entry, and one mapping it elsewhere got the entry in a file pull, doctor and uninstall do not read. It writes the opencode.json beside the file again, as they read it. * fix(uninstall): remove teamai's MCP servers from a file the tool left (#1221) After a home move, uninstall dropped the records of servers an earlier release wrote to the old file and left the servers there. It now takes the unchanged ones out, as the local agent's uninstall_mcp does; a copy the member changed stays. * feat(locate): follow CodeBuddy's home when CODEBUDDY_CONFIG_DIR moves it (#1221) CodeBuddy 2.168.0 reads its whole home from CODEBUDDY_CONFIG_DIR, not ~/.codebuddy, and keeps .codebuddy.json there too. teamai assumed the variable moved only .codebuddy.json, so skills, rules, CODEBUDDY.md, agents and the user MCP files went to a home CodeBuddy did not read. The variable is now a home move, recorded as the others are, so every shell writes the same place. Hooks stay in ~/.codebuddy/settings.json until B4, and doctor notes it. * fix(uninstall): remove local-scope MCP servers from a file the tool left (#1221) With sharing.gitExclude off, uninstall took teamai's servers out of the local scope's current file only. After CodeBuddy's home moved, it dropped the records of servers in the old .codebuddy.json and left the servers. It now removes the unchanged ones there, as it does for a target. * fix(mcp): take servers from the file their record names after CodeBuddy's home moves (#1221) Once CODEBUDDY_CONFIG_DIR moves CodeBuddy's home, a record names a file the tool left. Three paths still used the current file: - A record an earlier release wrote with no file was taken to own the moved mapped file, so a pull could overwrite a member's server there. It now resolves to the mapped file under the default home. - The local agent's uninstall_mcp removed nothing from the recorded .codebuddy.json and dropped the record. - Uninstall and pull took other checkouts' servers out of the current .codebuddy.json only, and dropped their records. They now go by the file each record names, as the orphan cleanup already did. Test setup clears CODEBUDDY_CONFIG_DIR with the other home variables. * test: clear every variable that moves a tool home, as TOOLS declares it (#1221) The test setup listed the home variables by hand and missed CODEBUDDY_CONFIG_DIR, so a test that stubs only HOME wrote fixtures into a developer's real CodeBuddy home. It now takes them from the tool descriptors, as init does: homeVariables moves to tools.ts beside the descriptors so both read one list. * fix(uninstall): take teamai's servers out of a Codex config.toml a moved root left (#1221) Uninstall cleaned the file a tool left after its root moved with the JSON writer only. A Codex config.toml there did not parse, so its servers stayed. Every edit of an MCP file now goes through one writer that picks the file's format, and a Codex block the member changed stays, as a JSON entry does. * fix(locate): follow the user config's tool roots from a project that records none (#1221) A project config without toolRoots took the tool's default home for everything locate reads from the tool's user config: the Codex hook gate of rules and blocks, Codex's trust config for agents and MCP, and Claude's local MCP scope. With Codex placed elsewhere by the user config and ~/.codex absent, a project pull skipped instruction delivery. The user view now takes the user config's roots in that case, as resolveMemberToolRoots and the earlier resolver did. * fix(uninstall): clean each MCP record in the file it names, OpenCode's moved project file too (#1221) Uninstall handed every OpenCode record outside the current file to the alternate file, so after toolPaths.opencode.mcpProject moved, teamai's servers stayed in the original opencode.json and their records were dropped. Records now go to the file they name. A record whose file no longer parses is kept for the retry instead of being read as gone. * fix(uninstall): clean recorded MCP files when the current one cannot be read (#1221) Uninstall now cleans each record in the file it names whatever state the current target is in, and a Codex config.toml that cannot be read keeps its records, as an unparsable JSON file does. * fix(uninstall): clean recorded MCP files whatever state the current one is in, in the scope a project's servers left too (#1221) * fix(mcp): tell a retired MCP key apart from the current one in one file, and keep what a kept record says of resolved values (#1221) * fix(mcp): keep an earlier release's MCP record while the retired file it may sit in cannot be read (#1221) * fix(uninstall): keep the data home while an MCP config holding teamai's servers does not parse (#1221) * fix(uninstall): keep the data home while a removed worktree's MCP servers sit in a config that does not parse (#1221) --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: teamai-ci <teamai-ci@noreply>
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
Add Copilot CLI MCP synchronization for user and project scopes. TeamAI now renders Copilot's native MCP entries, honors
COPILOT_HOME, preserves both wrapped user configuration and bare project server maps, rejects unmanaged collisions, and reconciles managed servers without modifying Copilot settings.Type of Change
Test Plan
npx tsc --noEmitpassesnpx vitest runpassesAdded/updated tests for the change
RED: focused regression tests failed for missing Copilot targets,
COPILOT_HOMEpath resolution, bare project-map preservation, and unmanaged collision detection before their fixes.GREEN: the full ignored
run-ci.shpasses (254 unit files / 3,515 tests; E2E 37 passing files / 3 skipped, 159 passing tests / 26 intentional skips).After
npm run build, the real built-CLI regression matrix passed all 12 combinations: Claude, Codex, CodeBuddy, and OpenCode acrossgit,gitlab, andgithubproviders.Changed executable lines have 100% coverage (69/69).
Authenticated Copilot CLI E2E with pinned
@github/copilot@1.0.86invoked the TeamAI-managed MCP tool exactly once and returned the expected proof marker.Verified user-owned MCP entries and Copilot settings remain unchanged, while TeamAI ownership markers remain intact.
Related Issues
Fixes #610
Notes for Reviewers
Copilot user scope resolves from
COPILOT_HOMEwhen set; project scope remains.github/mcp.json. Local servers include Copilot's requiredtype,tools,command, andargsfields, while remote HTTP/SSE servers includetype,tools, andurl.