Skip to content

Commit 6559142

Browse files
committed
feat(sdk): report project MCP servers overriding user entries as trust-gated
1 parent c16fd4f commit 6559142

2 files changed

Lines changed: 60 additions & 8 deletions

File tree

packages/node-sdk/src/sdk-rpc-client-v2.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,11 @@ import {
148148
} from '@pymodel/agent-core';
149149
import { encodeWorkDirKey } from '@pymodel/agent-core-v2/_base/utils/workdir-slug';
150150
import { McpConnectionManager } from '@pymodel/agent-core-v2/mcpCore/connection-manager';
151-
import { loadMcpServers } from '@pymodel/agent-core-v2/app/mcpConfig/configLoader';
151+
import {
152+
loadMcpServers,
153+
loadMcpServersDetailed,
154+
resolveMcpJsonPaths,
155+
} from '@pymodel/agent-core-v2/app/mcpConfig/configLoader';
152156
import { IAppendLogStore } from '@pymodel/agent-core-v2/persistence/interface/appendLogStore';
153157
import type { McpServerConfig as WorkspaceMcpServerConfig } from '@pymodel/agent-core-v2/mcpCore/config-schema';
154158
import {
@@ -629,8 +633,8 @@ export class SDKRpcClientV2 extends SDKRpcClientBase {
629633
* via {@link engineAccessor} — the same `handlerFor({ root })` path
630634
* `createSession` takes (materializing the workspace handler is a no-op
631635
* cost here: session creation does it anyway). The gated-server list is
632-
* what the pure config loader sees with project files included vs skipped
633-
* (the workspaceTrust gate inside the engine's `workspaceMcpConfig`),
636+
* the final merged config entries whose origins are project files (the
637+
* workspaceTrust gate inside the engine's `workspaceMcpConfig`),
634638
* computed best-effort: an unreadable/invalid project file degrades to an
635639
* empty list rather than failing the caller.
636640
*/
@@ -642,12 +646,18 @@ export class SDKRpcClientV2 extends SDKRpcClientBase {
642646
if (trusted) return { trusted: true, gatedMcpServers: [] };
643647
try {
644648
const fs = this.engineAccessor.get(IHostFileSystem);
645-
const [withProject, userOnly] = await Promise.all([
646-
loadMcpServers({ fs, cwd: workDir, homeDir: this.homeDir, includeProject: true }),
647-
loadMcpServers({ fs, cwd: workDir, homeDir: this.homeDir, includeProject: false }),
649+
const [paths, loaded] = await Promise.all([
650+
resolveMcpJsonPaths({ fs, cwd: workDir, homeDir: this.homeDir }),
651+
loadMcpServersDetailed({
652+
fs,
653+
cwd: workDir,
654+
homeDir: this.homeDir,
655+
includeProject: true,
656+
}),
648657
]);
649-
const gatedMcpServers = Object.entries(withProject)
650-
.filter(([name]) => !(name in userOnly))
658+
const projectPaths = new Set([paths.projectRoot, paths.project]);
659+
const gatedMcpServers = Object.entries(loaded.servers)
660+
.filter(([name]) => projectPaths.has(loaded.origins[name] ?? ''))
651661
.map(([name, config]) => describeWorkspaceMcpServer(name, config))
652662
.toSorted((a, b) => a.name.localeCompare(b.name));
653663
return { trusted: false, gatedMcpServers };

packages/node-sdk/test/sdk-rpc-client-v2.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,48 @@ describe('SDKRpcClientV2 workspace trust', () => {
974974
await harness.close();
975975
}
976976
});
977+
978+
it('reports project servers that override same-named user entries', async () => {
979+
const { harness, homeDir } = await makeHarness();
980+
const workDir = await mkdtemp(join(tmpdir(), 'pythinker-sdk-v2-work-'));
981+
tempDirs.push(workDir);
982+
await writeFile(
983+
join(homeDir, 'mcp.json'),
984+
JSON.stringify({
985+
mcpServers: {
986+
github: { command: 'user-github', enabled: false },
987+
},
988+
}),
989+
'utf-8',
990+
);
991+
await writeFile(
992+
join(workDir, '.mcp.json'),
993+
JSON.stringify({
994+
mcpServers: {
995+
github: { command: 'project-github', enabled: false },
996+
toString: { transport: 'http', url: 'https://example.test/mcp', enabled: false },
997+
},
998+
}),
999+
'utf-8',
1000+
);
1001+
try {
1002+
const info = await harness.getWorkspaceTrustInfo(workDir);
1003+
expect(info.trusted).toBe(false);
1004+
expect(info.gatedMcpServers).toEqual([
1005+
{
1006+
name: 'github',
1007+
transport: 'stdio',
1008+
command: 'project-github',
1009+
args: undefined,
1010+
cwd: workDir,
1011+
},
1012+
{ name: 'toString', transport: 'http', url: 'https://example.test/mcp' },
1013+
]);
1014+
} finally {
1015+
await harness.close();
1016+
}
1017+
});
1018+
9771019
});
9781020

9791021
describe('foldAgentWireReplay', () => {

0 commit comments

Comments
 (0)