Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
ab34ab2
feat(mcp): keep project MCP configs with resolved values out of git (…
SaulMoro Sep 28, 2026
371b6d4
fix(uninstall): count the .git/info/exclude block in the removal plan…
SaulMoro Sep 28, 2026
ec29e34
fix(mcp): keep the exclude block until every MCP config is clean (#882)
SaulMoro Sep 28, 2026
dba5374
fix(uninstall): remove the exclude block only once its files are prov…
SaulMoro Sep 28, 2026
336a3b0
fix(mcp): protect every project MCP config holding a resolved value (…
SaulMoro Sep 28, 2026
0abdc1e
fix(mcp): judge MCP configs by disk and manifest, not current config …
SaulMoro Sep 28, 2026
d8da567
fix(mcp): skip the .git/info/exclude write while another command hold…
SaulMoro Sep 28, 2026
b1a7973
fix(uninstall): keep an exclude entry unless its MCP config is proven…
SaulMoro Sep 28, 2026
38a8fb2
fix(mcp): exclude a project MCP config from git before writing a reso…
SaulMoro Sep 29, 2026
71c27e7
fix(mcp): report a server withheld from a file git would commit in mc…
SaulMoro Sep 29, 2026
a9f0836
fix(mcp): report a tracked file on a dry run and a withheld server al…
SaulMoro Sep 29, 2026
c4a9160
fix(mcp): name a tracked MCP config before an unwritable .git/info/ex…
SaulMoro Sep 29, 2026
683a36a
fix(mcp): take a project MCP config's exclude line back out once it h…
SaulMoro Sep 29, 2026
ae6d4a6
fix(mcp): judge a project MCP config by the manifest as it stood befo…
SaulMoro Sep 29, 2026
389635e
fix(mcp): log a rolled-back exclude line at debug level (#882)
SaulMoro Sep 29, 2026
06f3b8d
fix(mcp): keep a shared exclude line while another worktree's config …
SaulMoro Sep 29, 2026
7810e52
test(mcp): build the other worktree from the real temp path so the te…
SaulMoro Sep 29, 2026
c58846c
Merge remote-tracking branch 'origin/main' into feat/882-mcp-git-exclude
SaulMoro Sep 29, 2026
e5331ff
fix(uninstall): list no worktrees for a project root that no longer e…
SaulMoro Sep 29, 2026
b040124
fix(mcp): treat an empty, unparsable or tool-less managed-mcp.json as…
SaulMoro Sep 29, 2026
5783dc6
fix(mcp): keep the exclude line of a config this pull wrote when a la…
SaulMoro Sep 29, 2026
d045b59
fix(mcp): read a check-ignore error as unsafe unless ls-files proves …
SaulMoro Sep 29, 2026
b9529d1
fix(mcp): report withheld only for targets delivery would write the s…
SaulMoro Sep 29, 2026
34bb7c1
docs(mcp): describe the .git/info/exclude block in the setup skill an…
SaulMoro Sep 29, 2026
875c7cf
fix(mcp): keep the exclude line of an entry a pull wrote with a resol…
SaulMoro Sep 29, 2026
7cd4841
fix(mcp): judge a nested repository's linked worktree config by its s…
SaulMoro Sep 29, 2026
a6cfbee
feat(mcp): record the project MCP configs a pull wrote a resolved val…
SaulMoro Sep 29, 2026
4339efb
fix(mcp): keep protecting a config a pull wrote under a toolPaths map…
SaulMoro Sep 29, 2026
5c46d8a
fix(mcp): keep a config's exclude line past the pull that rebuilt its…
SaulMoro Sep 29, 2026
c43085b
test(mcp): pin today's exclude rules for a missing, corrupt or locked…
SaulMoro Sep 29, 2026
23a00df
docs(mcp): describe managed-mcp-files.json and the configs it keeps p…
SaulMoro Sep 29, 2026
0ec4246
refactor(mcp): keep the #882 record edits off the lines #880 changes …
SaulMoro Sep 29, 2026
72799f5
Merge remote-tracking branch 'origin/main' into feat/882-mcp-git-exclude
SaulMoro Sep 29, 2026
53c4216
fix(mcp): protect a config an older teamai wrote under a mapping an e…
SaulMoro Sep 29, 2026
a466fd5
fix(mcp): keep a rebuilt record from persisting without its note of t…
SaulMoro Sep 29, 2026
a933c22
fix(mcp): take back a managed-mcp-files.json record for a config the …
SaulMoro Sep 29, 2026
df98441
docs(mcp): describe the teamai.yaml history read, the unnoted rebuilt…
SaulMoro Sep 29, 2026
ecb3009
refactor(mcp): keep the r3 edits off the lines #880 changes (#882)
SaulMoro Sep 29, 2026
16007ae
fix(mcp): also protect a config an older teamai wrote under a built-i…
SaulMoro Sep 29, 2026
c252a0e
fix(mcp): judge a project MCP config under a symlinked directory wher…
SaulMoro Sep 29, 2026
5152b74
docs(mcp): describe how a config under a symlinked directory is kept …
SaulMoro Sep 29, 2026
d19a6d6
refactor(mcp): keep realFilePath next to existingAncestor, without an…
SaulMoro Sep 29, 2026
0c8d2dc
fix(mcp): judge a config under an earlier teamai.yaml mapping as a re…
SaulMoro Sep 29, 2026
a30f56d
fix(mcp): have doctor check the configs earlier teamai.yaml mappings …
SaulMoro Sep 29, 2026
5d9df20
docs(mcp): describe how a config under an earlier teamai.yaml mapping…
SaulMoro Sep 29, 2026
bf28224
Merge remote-tracking branch 'origin/main' into feat/882-mcp-git-exclude
SaulMoro Sep 29, 2026
5696b36
fix(mcp): record a config under an earlier teamai.yaml mapping that g…
SaulMoro Sep 29, 2026
fbed13d
fix(mcp): keep judging a recorded config for a tool the team moved wh…
SaulMoro Sep 29, 2026
16c1938
docs(mcp): describe the tracked config an earlier mapping reached, an…
SaulMoro Sep 29, 2026
fc807e0
fix(mcp): find a config an older teamai wrote under an earlier mappin…
SaulMoro Sep 29, 2026
c4d39c1
docs(mcp): describe the history read's configs another tool maps toda…
SaulMoro Sep 29, 2026
e46c8a0
Merge remote-tracking branch 'origin/main' into feat/882-mcp-git-exclude
SaulMoro Sep 29, 2026
3bea844
fix(mcp): prove a shared config clean only while every tool that wrot…
SaulMoro Sep 29, 2026
3d718b8
fix(mcp): judge a built-in location no mapping reaches today as an ea…
SaulMoro Sep 29, 2026
da0c8f8
docs(mcp): describe the built-in location of a moved or dropped tool,…
SaulMoro Sep 29, 2026
32fa951
fix(mcp): name the ignore rule that re-includes a config teamai just …
SaulMoro Sep 29, 2026
cd69dad
fix(mcp): judge a moved tool's built-in location another tool maps fo…
SaulMoro Sep 29, 2026
978ecec
docs(mcp): describe the no-manifest rule, a moved tool's built-in loc…
SaulMoro Sep 29, 2026
9fb467c
Merge remote-tracking branch 'origin/main' into feat/882-mcp-git-exclude
SaulMoro Sep 29, 2026
0f6db9e
fix(mcp): keep a tool's record as it was when its config does not par…
SaulMoro Sep 29, 2026
6b177e8
fix(mcp): mark the records a pull with no managed-mcp.json writes as …
SaulMoro Sep 29, 2026
95ee768
fix(mcp): have doctor judge a record marked unnoted like a missing ma…
SaulMoro Sep 29, 2026
a915ed1
fix(mcp): judge a config tools of different formats share in each of …
SaulMoro Sep 29, 2026
c544a38
fix(mcp): note the servers no record claims in the file of a tool who…
SaulMoro Sep 29, 2026
dfe1a74
fix(mcp): take back a tool managed-mcp-files.json recorded before a w…
SaulMoro Sep 29, 2026
549bcab
fix(mcp): treat an installed tool's missing record as lost at every p…
SaulMoro Sep 29, 2026
9ba907c
fix(mcp): note the unclaimed servers under every format of a shared c…
SaulMoro Sep 29, 2026
6e5f659
fix(mcp): count an uninstalled tool's missing record when no installe…
SaulMoro Sep 29, 2026
00169ad
fix(mcp): scope a shared config's claims to the tools reading the sam…
SaulMoro Sep 29, 2026
e3eb882
Merge remote-tracking branch 'origin/main' into feat/882-mcp-git-exclude
SaulMoro Sep 29, 2026
2033b5b
fix(mcp): keep suspect an uninstalled tool managed-mcp-files.json lis…
SaulMoro Sep 29, 2026
4d01d7f
fix(mcp): judge a moved tool's file by the records of the tools readi…
SaulMoro Sep 29, 2026
174101e
fix(mcp): read a Copilot project config's bare servers beside the mcp…
SaulMoro Sep 29, 2026
f80b871
fix(mcp): keep a project config the local agent writes a header or en…
SaulMoro Sep 29, 2026
6721cf4
fix(mcp): document the local agent's project-scope exclusion and Copi…
SaulMoro Sep 29, 2026
afbf42c
fix(mcp): tell the local agent's withheld install to install the MCP …
SaulMoro Sep 29, 2026
bca30eb
fix(mcp): replace a Copilot server's bare copy when pull or the local…
SaulMoro Sep 29, 2026
4b76d70
fix(mcp): count a local-agent install's arguments, URL user or query …
SaulMoro Sep 29, 2026
cfffd1a
fix(mcp): count any URL in a local-agent install as a credential, and…
SaulMoro Sep 29, 2026
c071702
fix(mcp): read OpenCode's command array, remove only teamai's own bar…
SaulMoro Sep 29, 2026
5c13481
fix(mcp): list a project config an older local agent wrote a credenti…
SaulMoro Sep 29, 2026
aa7ff2d
fix(mcp): document that the local agent's sync and pull list an older…
SaulMoro Sep 29, 2026
de6ae4c
fix(mcp): have the local agent's sync say a new session tries again a…
SaulMoro Sep 29, 2026
d19c516
fix(mcp): run the local agent's sync protection after an uninstall_te…
SaulMoro Sep 29, 2026
7ffd559
Merge remote-tracking branch 'origin/main' into feat/882-mcp-git-exclude
SaulMoro Sep 30, 2026
8b8d9fc
fix(mcp): judge a local-agent entry a failed write left by what it ho…
SaulMoro Sep 30, 2026
17b1a59
fix(mcp): keep a moved Copilot's stale bare server apart from a name …
SaulMoro Sep 30, 2026
9c3333d
fix(mcp): have the local agent's protection read CodeBuddy's former d…
SaulMoro Sep 30, 2026
4e71c7a
fix(mcp): prove bare ownership and persist installs before exclusions…
SaulMoro Sep 30, 2026
1a4ddfa
fix(mcp): keep bare ownership from claiming keyed Copilot entries (#882)
SaulMoro Sep 30, 2026
c0748a9
fix(mcp): require evidence for unmarked Copilot ownership (#882)
SaulMoro Sep 30, 2026
d548eaa
fix(mcp): preserve ownership across failed config updates (#882)
SaulMoro Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions docs/designs/data-directory-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,13 +439,23 @@ every checkout, so that is where they live now:
├── reports-wt/ (the side-branch locks sit beside them)
├── pending-learnings/ pendingLearningsDir → <dataHome>/pending-learnings
└── workspaces/<managedMcpWorkspaceId(root)>/
├── managed-mcp.json managedMcpManifestPath, one per checkout
├── managed-mcp.json managedMcpManifestPath, one per checkout; Copilot placement is true for bare, false for keyed, absent when unproven
├── managed-mcp-files.json resolvedMcpFilesPath: project MCP configs teamai may have written a resolved ${VAR} to, and whether
│ the paths earlier teamai.yaml revisions mapped were read; one of those git tracks is marked tracked (#882)
│ the paths earlier teamai.yaml revisions mapped were read; one of those git tracks is marked tracked (#882);
│ for an HTTP team, the configs the local agent wrote a credential to
└── search-index.json getProjectSearchIndexPath, one per checkout
<checkout>/.teamai/ one per checkout: committed knowledge, knowledge-wt/
```

MCP configs and ownership must describe the same completed writes. Existing
JSON local-agent installs keep the old record until the config write succeeds;
uninstall keeps it until the entry is removed. A later manifest-write failure
restores the previous config. Reconcile keeps one snapshot per config before
any tool writes it and restores those snapshots if saving ownership or a later
config write fails. File records added by that failed run are cleaned up before
Git protection is checked against the restored configs. If restoration also
fails, the command reports both failures and keeps credential files excluded.

`git worktree add` takes a path outside the repo, and the owning repo is still
the business repo, whose refs every checkout shares. The search index is keyed
per checkout, like managed MCP, because each checkout indexes its own branch's
Expand Down
6 changes: 5 additions & 1 deletion docs/usage-guide.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion docs/usage-guide.zh-CN.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions skill-data/core/references/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ ever committed with it; then `teamai pull`. Do not run `git rm` or commit for
them. For an exclude file that is not writable, one another teamai command
held, or a git error, relay the fix the line gives.

For new HTTP local-agent MCP installs, a failed initial ownership-manifest write leaves the MCP config and Git exclusions unchanged. Retry the install after fixing the manifest write error. A bare Copilot entry beside `mcpServers` is removed only with a matching ownership record proving a completed bare write. Older records without that evidence preserve the bare entry. A bare ownership record cannot claim a same-named member entry under `mcpServers`: updates skip the collision, and removal leaves that keyed entry alone. An unmarked Copilot record needs a matching keyed hash that does not also match the bare entry. Completed writes record `bare: true` or `bare: false`; missing placement remains unproven, including after a failed placement-record write. Existing JSON MCP updates keep the old ownership until the config write completes; a later manifest failure restores the config. `uninstall_mcp` keeps ownership if reading or writing the config fails, and restores the entry if removing its manifest record fails. MCP reconcile also restores all configs written before an ownership-save failure. If restoration fails too, repair the named configs and ownership records before retrying; the error reports both failures, and configs still carrying credentials stay excluded from Git.

## Permission / access denied

`init`, `pull`, or `push` failing with a permission error usually means the user
Expand Down
7 changes: 6 additions & 1 deletion skill-data/setup/references/manage-admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ and that server noted: it keeps the line until it leaves the file. So is the
file of a tool `managed-mcp.json` has no record for, when a pull writes that
tool's first record (its record lost, or teamai's first delivery to it). While that
note cannot be written (another teamai command holds the record), the line stays
until a later pull writes it.
until a later pull writes it. A Copilot project config's bare top-level servers
still count once another tool writes `mcpServers` into the file. On 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 (only a bare stdio command is not), fails the install when it cannot, and only
`teamai uninstall` takes that line out. The next sync or `teamai pull` in the
workspace also lists a file an older local agent wrote a credential into; `teamai doctor` checks those files too.

## Invite a member

Expand Down
129 changes: 129 additions & 0 deletions src/__tests__/doctor-mcp-delivery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,135 @@ describe('doctor — MCP servers delivered on disk', () => {
expect(check.fix).not.toContain(path.join(projectRoot, '.mcp.json'));
});

it('fails for a moved tool\'s file while the tool mapping it today owns that server name only under another key', async () => {
const { trackResolvedMcpFiles } = await import('../mcp-resolved-files.js');
teamConfig.toolPaths = { ...teamConfig.toolPaths, opencode: { skills: '.opencode/skills', mcp: '.config/opencode/opencode.json', mcpProject: 'shared/mcp.json' } };
const shared = path.join(projectRoot, 'shared', 'mcp.json');
await fse.outputJson(shared, {
mcpServers: { x: { type: 'http', url: 'https://x.example/mcp', headers: { Authorization: 'Bearer t0ken-of-cursor' } } },
mcp: { x: { type: 'remote', url: 'https://x.example/mcp' } },
});
expect(await trackResolvedMcpFiles(localConfig, [{ tool: 'cursor', file: shared }])).toBe('written');
await fse.outputJson(managedMcpManifestPath(getDataHome(localConfig), projectRoot), {
[managedMcpManifestKey('opencode', true)]: [{ name: 'x', hash: 'fixture-hash', resolved: false }],
});
await fse.appendFile(path.join(projectRoot, '.git', 'info', 'exclude'), '/.mcp.json\n');

const check = await excludeCheck();
if (!check) throw new Error('no git exclude check');
expect(await check.check()).toBe(false);
expect(check.fix ?? '').toContain(shared);
});

it('fails for a moved Copilot\'s file while the tool mapping it today owns that server name only under mcpServers', async () => {
const { trackResolvedMcpFiles } = await import('../mcp-resolved-files.js');
teamConfig.toolPaths = {
...teamConfig.toolPaths,
cursor: { skills: '.cursor/skills', mcp: '.cursor/mcp.json', mcpProject: 'shared/mcp.json' },
copilot: { skills: '.github/skills', mcp: '.copilot/mcp-config.json', mcpProject: '.github/mcp.json' },
};
const shared = path.join(projectRoot, 'shared', 'mcp.json');
await fse.outputJson(shared, {
x: { type: 'http', url: 'https://x.example/mcp', headers: { Authorization: 'Bearer t0ken-of-copilot' } },
mcpServers: { x: { type: 'http', url: 'https://x.example/mcp' } },
});
expect(await trackResolvedMcpFiles(localConfig, [{ tool: 'copilot', file: shared }])).toBe('written');
await fse.outputJson(managedMcpManifestPath(getDataHome(localConfig), projectRoot), {
[managedMcpManifestKey('cursor', true)]: [{ name: 'x', hash: 'fixture-hash', resolved: false }],
// Copilot's record describes the file its mapping reaches today, not this one.
[managedMcpManifestKey('copilot', true)]: [],
});
// Cursor's x is still the team's, now a literal: its own rules find nothing to keep.
await writeTeamMcp('servers:\n - name: x\n transport: http\n url: https://x.example/mcp\n');
await fse.appendFile(path.join(projectRoot, '.git', 'info', 'exclude'), '/.mcp.json\n');

const check = await excludeCheck();
if (!check) throw new Error('no git exclude check');
expect(await check.check()).toBe(false);
expect(check.fix ?? '').toContain(shared);
});

describe('for an HTTP-backed team, judged by the records the local agent wrote', () => {
const writeRecord = (record: Record<string, unknown>): Promise<void> =>
fse.outputJson(managedMcpManifestPath(getDataHome(localConfig), projectRoot), { [managedMcpManifestKey('claude', true)]: [record] });

beforeEach(async () => {
Object.assign(localConfig, { repo: { localPath: repoPath, remote: 'https://teamai.example', kind: 'http', url: 'https://teamai.example' } });
// An HTTP team has no mcp.yaml: its servers arrive through install_mcp.
await fse.remove(path.join(repoPath, 'mcp'));
});

it.each([
['notes it carried a header or env value', { name: 'jira', hash: 'h', resolved: true }],
['is an older local agent\'s, without that note, and its entry holds a header', { name: 'jira', hash: 'h' }],
])('fails while git would track the file, and names it, when the record %s', async (_label, record) => {
await writeRecord(record);

const check = await excludeCheck();
if (!check) throw new Error('no git exclude check');
expect(await check.check()).toBe(false);
expect(check.fix).toContain(path.join(projectRoot, '.mcp.json'));
// No pull writes an HTTP team's servers, so none lists the file.
expect(check.fix).not.toContain('teamai pull');
});

it('passes once git ignores the file', async () => {
await writeRecord({ name: 'jira', hash: 'h', resolved: true });
await fse.appendFile(path.join(projectRoot, '.git', 'info', 'exclude'), '/.mcp.json\n');

const check = await excludeCheck();
if (!check) throw new Error('no git exclude check');
expect(await check.check()).toBe(true);
});

it('still fails for a file managed-mcp-files.json lists, holding a server, while the manifest has no record for it', async () => {
const { trackResolvedMcpFiles } = await import('../mcp-resolved-files.js');
await fse.remove(managedMcpManifestPath(getDataHome(localConfig), projectRoot));
expect(await trackResolvedMcpFiles(localConfig, [{ tool: 'claude', file: path.join(projectRoot, '.mcp.json') }])).toBe('written');

const check = await excludeCheck();
if (!check) throw new Error('no git exclude check');
expect(await check.check()).toBe(false);
expect(check.fix).toContain(path.join(projectRoot, '.mcp.json'));
});

it('still fails while a server carrying a credential has lost its record, though another server\'s remains', async () => {
await fse.writeJson(path.join(projectRoot, '.mcp.json'), {
mcpServers: {
jira: { type: 'http', url: 'https://jira.example/mcp', headers: { Authorization: 'Bearer t0ken' } },
local: { command: 'local-mcp' },
},
});
await writeRecord({ name: 'local', hash: 'h', resolved: false });

const check = await excludeCheck();
if (!check) throw new Error('no git exclude check');
expect(await check.check()).toBe(false);
});

it('fails for a credential a local agent from before 57636a27 wrote at CodeBuddy\'s former .codebuddy/mcp.json', async () => {
const old = path.join(projectRoot, '.codebuddy', 'mcp.json');
await fse.outputJson(old, { mcpServers: { clawpro: { type: 'http', url: 'https://clawpro.example/mcp', headers: { Authorization: 'Bearer t0ken' } } } });
await fse.outputJson(managedMcpManifestPath(getDataHome(localConfig), projectRoot), {
[managedMcpManifestKey('codebuddy', true)]: [{ name: 'clawpro', hash: 'h' }],
});

const check = await excludeCheck();
if (!check) throw new Error('no git exclude check');
expect(await check.check()).toBe(false);
expect(check.fix ?? '').toContain(old);
});

it('has nothing to say of a file whose recorded server carries neither header nor env value', async () => {
const jira = { type: 'http', url: 'https://jira.example/mcp' };
await fse.writeJson(path.join(projectRoot, '.mcp.json'), { mcpServers: { jira } });
const { entryHash } = await import('../resources/mcp-format.js');
await writeRecord({ name: 'jira', hash: entryHash(jira), resolved: false });

expect(await excludeCheck()).toBeUndefined();
});
});

describe('a config an older teamai wrote under a mapping an earlier teamai.yaml made, before a pull on this version', () => {
const old = (): string => path.join(projectRoot, '.cursor', 'team-mcp.json');
const commitTeamYaml = (toolPaths: object): void => {
Expand Down
Loading
Loading