From 306aa356439e800aff0974d74b833cae88e35fd4 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Wed, 2 Sep 2026 13:48:55 +0800 Subject: [PATCH 1/3] feat(agent-core-v2): support per-server deferred disclosure of MCP tools From b18d344bdf47638e9a9669324a524cd71184dd24 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Wed, 2 Sep 2026 11:34:52 +0800 Subject: [PATCH 2/3] feat(agent-core-v2): support per-server deferred disclosure of MCP tools --- .changeset/mcp-server-deferred-config.md | 5 ++++ docs/en/customization/mcp.md | 1 + docs/zh/customization/mcp.md | 1 + packages/agent-core-v2/src/agent/mcp/mcp.ts | 1 + .../agent-core-v2/src/agent/mcp/mcpService.ts | 8 ++++-- .../src/agent/toolSelect/toolSelectService.ts | 2 +- .../src/mcpCore/config-schema.ts | 1 + .../src/mcpCore/connection-manager.ts | 3 ++ .../fullCompaction/fullCompaction.test.ts | 2 +- .../agent-core-v2/test/agent/mcp/mcp.test.ts | 17 +++++++++++ .../agent/toolSelect/toolSelect.e2e.test.ts | 4 ++- .../toolSelect/toolSelectService.test.ts | 28 +++++++++++++++++-- .../test/mcpCore/connection-manager.test.ts | 14 ++++++++++ 13 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 .changeset/mcp-server-deferred-config.md diff --git a/.changeset/mcp-server-deferred-config.md b/.changeset/mcp-server-deferred-config.md new file mode 100644 index 00000000000..87fc41ca8fb --- /dev/null +++ b/.changeset/mcp-server-deferred-config.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Add a per-server `deferred` field to MCP server configuration: when the model supports dynamic tool loading (experimental `tool-select` flag), set `deferred: false` to keep a server's tools in the top-level tool list instead of loading them on demand via `select_tools`. diff --git a/docs/en/customization/mcp.md b/docs/en/customization/mcp.md index be65fd48b49..c13609091eb 100644 --- a/docs/en/customization/mcp.md +++ b/docs/en/customization/mcp.md @@ -56,6 +56,7 @@ Optional fields: | `headers` | `Record` | HTTP, SSE | Static request headers appended to every request | | `bearerTokenEnvVar` | `string` | HTTP, SSE | Name of an environment variable that contains a bearer token | | `enabled` | `boolean` | All | Set to `false` to disable this server | +| `deferred` | `boolean` | All | Experimental: with the `tool-select` flag and a model that declares dynamically loaded tools, this server's tools stay out of the top-level tool list and are loaded on demand via `select_tools`; set to `false` to always expose them inline. Defaults to `true`; ignored while the flag is off | | `startupTimeoutMs` | `number` | All | Connection timeout from `1` to `2147483647` milliseconds; default `30000` | | `toolTimeoutMs` | `number` | All | Timeout from `1` to `2147483647` milliseconds for a single tool call | | `enabledTools` | `string[]` | All | Tool allowlist | diff --git a/docs/zh/customization/mcp.md b/docs/zh/customization/mcp.md index f09da69c36f..90192c87138 100644 --- a/docs/zh/customization/mcp.md +++ b/docs/zh/customization/mcp.md @@ -56,6 +56,7 @@ MCP server 配置写在 `mcp.json` 中,分两层: | `headers` | `Record` | HTTP、SSE | 附加到每次请求的静态请求头 | | `bearerTokenEnvVar` | `string` | HTTP、SSE | 存放 bearer token 的环境变量名 | | `enabled` | `boolean` | 全部 | 设为 `false` 可禁用该 server | +| `deferred` | `boolean` | 全部 | 实验功能:启用 `tool-select` 标志且模型声明动态工具加载能力时,该 server 的工具默认不进入顶层工具列表,由模型通过 `select_tools` 按需加载;设为 `false` 则始终直接暴露。默认 `true`;标志未启用时该字段无效 | | `startupTimeoutMs` | `number` | 全部 | 连接超时,取值范围为 `1` 到 `2147483647` 毫秒,默认 `30000` | | `toolTimeoutMs` | `number` | 全部 | 单次工具调用超时,取值范围为 `1` 到 `2147483647` 毫秒 | | `enabledTools` | `string[]` | 全部 | 工具白名单 | diff --git a/packages/agent-core-v2/src/agent/mcp/mcp.ts b/packages/agent-core-v2/src/agent/mcp/mcp.ts index 0e47b09725d..6fd94ff05d0 100644 --- a/packages/agent-core-v2/src/agent/mcp/mcp.ts +++ b/packages/agent-core-v2/src/agent/mcp/mcp.ts @@ -11,6 +11,7 @@ export interface McpResolvedServer { readonly tools: readonly KosongTool[]; readonly rawTools: readonly MCPToolDefinition[]; readonly enabledNames: ReadonlySet; + readonly deferred: boolean; } export interface IAgentMcpService { diff --git a/packages/agent-core-v2/src/agent/mcp/mcpService.ts b/packages/agent-core-v2/src/agent/mcp/mcpService.ts index bded747b103..abee7ebe1bf 100644 --- a/packages/agent-core-v2/src/agent/mcp/mcpService.ts +++ b/packages/agent-core-v2/src/agent/mcp/mcpService.ts @@ -211,6 +211,7 @@ export class AgentMcpService extends Service implements IAgentMcpService { resolved.client, resolved.tools, resolved.enabledNames, + resolved.deferred, ); this.emitMcpToolCollisions(entry.name, result.collisions); this.recordDiscovery(entry.name, resolved.rawTools, resolved.enabledNames, result.collisions); @@ -234,7 +235,9 @@ export class AgentMcpService extends Service implements IAgentMcpService { oauthService, reconnect: (signal) => this.reconnect(entry.name, signal), }); - const disposable = this._register(this.registry.register(tool, { source: 'mcp' })); + const disposable = this._register( + this.registry.register(tool, { source: 'mcp', disclosure: 'deferred' }), + ); this.mcpTools.set(tool.name, { disposable, serverName: entry.name }); this.mcpToolsByServer.set(entry.name, [tool.name]); void this.dispatcher.dispatch( @@ -251,6 +254,7 @@ export class AgentMcpService extends Service implements IAgentMcpService { client: MCPClient, tools: readonly KosongTool[], enabledTools: ReadonlySet, + deferred: boolean, ): { readonly registered: readonly string[]; readonly collisions: readonly McpToolCollision[]; @@ -290,7 +294,7 @@ export class AgentMcpService extends Service implements IAgentMcpService { isRemoved: () => this.mcpHandle.connectionManager.get(serverName)?.status === 'removed', }), - { source: 'mcp' }, + { source: 'mcp', disclosure: deferred ? 'deferred' : 'inline' }, ), ); this.mcpTools.set(qualified, { disposable, serverName }); diff --git a/packages/agent-core-v2/src/agent/toolSelect/toolSelectService.ts b/packages/agent-core-v2/src/agent/toolSelect/toolSelectService.ts index 208e3cefdd5..a3f21b6eeeb 100644 --- a/packages/agent-core-v2/src/agent/toolSelect/toolSelectService.ts +++ b/packages/agent-core-v2/src/agent/toolSelect/toolSelectService.ts @@ -237,7 +237,7 @@ export class AgentToolSelectService extends Service implements IAgentToolSelectS } private isDynamicallyLoadable(info: ToolInfo): boolean { - return info.source === 'mcp' || info.disclosure === 'deferred'; + return info.disclosure === 'deferred'; } private shapeActiveHistory(messages: readonly ContextMessage[]): readonly ContextMessage[] { diff --git a/packages/agent-core-v2/src/mcpCore/config-schema.ts b/packages/agent-core-v2/src/mcpCore/config-schema.ts index 83afa8c70cf..e88a81dc20d 100644 --- a/packages/agent-core-v2/src/mcpCore/config-schema.ts +++ b/packages/agent-core-v2/src/mcpCore/config-schema.ts @@ -7,6 +7,7 @@ export const McpTimeoutMsSchema = z.number().int().min(1).max(MAX_MCP_TIMEOUT_MS const McpServerCommonFields = { enabled: z.boolean().optional(), + deferred: z.boolean().optional(), startupTimeoutMs: McpTimeoutMsSchema.optional(), toolTimeoutMs: McpTimeoutMsSchema.optional(), enabledTools: z.array(z.string()).optional(), diff --git a/packages/agent-core-v2/src/mcpCore/connection-manager.ts b/packages/agent-core-v2/src/mcpCore/connection-manager.ts index 72a94554c0c..2098ec0fd43 100644 --- a/packages/agent-core-v2/src/mcpCore/connection-manager.ts +++ b/packages/agent-core-v2/src/mcpCore/connection-manager.ts @@ -49,6 +49,7 @@ export interface McpConnectionView { tools: readonly Tool[]; rawTools: readonly MCPToolDefinition[]; enabledNames: ReadonlySet; + deferred: boolean; } | undefined; getRemoteServerUrl(name: string): string | undefined; @@ -144,6 +145,7 @@ export class McpConnectionManager implements McpConnectionView { tools: readonly Tool[]; rawTools: readonly MCPToolDefinition[]; enabledNames: ReadonlySet; + deferred: boolean; } | undefined { const entry = this.entries.get(name); @@ -160,6 +162,7 @@ export class McpConnectionManager implements McpConnectionView { tools: entry.tools, rawTools: entry.rawTools, enabledNames: entry.enabledNames ?? new Set(entry.tools.map((t) => t.name)), + deferred: entry.config.deferred !== false, }; } diff --git a/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts b/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts index 7acea74ee7d..3d6bb8a2651 100644 --- a/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts +++ b/packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts @@ -2045,7 +2045,7 @@ describe('FullCompaction', () => { }); const registration = ctx .get(IAgentToolRegistryService) - .register(mcpTool(LARGE_MCP_TOOL, parameters), { source: 'mcp' }); + .register(mcpTool(LARGE_MCP_TOOL, parameters), { source: 'mcp', disclosure: 'deferred' }); try { ctx.context.append({ role: 'system', diff --git a/packages/agent-core-v2/test/agent/mcp/mcp.test.ts b/packages/agent-core-v2/test/agent/mcp/mcp.test.ts index 3cfb38d8f04..17f78c964ee 100644 --- a/packages/agent-core-v2/test/agent/mcp/mcp.test.ts +++ b/packages/agent-core-v2/test/agent/mcp/mcp.test.ts @@ -50,6 +50,7 @@ interface ResolvedServer { readonly tools: readonly KosongTool[]; readonly rawTools: readonly MCPToolDefinition[]; readonly enabledNames: ReadonlySet; + readonly deferred: boolean; } class FakeMcpManager { @@ -118,6 +119,7 @@ class FakeMcpManager { tools: readonly KosongTool[], enabledNames = new Set(tools.map((tool) => tool.name)), rawTools?: readonly MCPToolDefinition[], + deferred = true, ): void { const resolvedRawTools = rawTools ?? @@ -131,6 +133,7 @@ class FakeMcpManager { tools, rawTools: resolvedRawTools, enabledNames, + deferred, }); } @@ -313,6 +316,7 @@ describe('AgentMcpService', () => { 'mcp__local_server__echo', 'mcp__local_server__noop', ]); + expect(infos.every((info) => info.disclosure === 'deferred')).toBe(true); expect(events).toContainEqual( expect.objectContaining({ type: 'tool.list.updated', @@ -322,6 +326,19 @@ describe('AgentMcpService', () => { ); }); + it('registers tools of a deferred=false server with inline disclosure', async () => { + const manager = new FakeMcpManager(); + const client = fakeMcpClient(); + manager.setResolved('s', client, await discoverTools(client), undefined, undefined, false); + createService(manager); + + manager.connect('s'); + + const infos = ix.get(IAgentToolRegistryService).list().filter((tool) => tool.source === 'mcp'); + expect(infos.length).toBeGreaterThan(0); + expect(infos.every((info) => info.disclosure === 'inline')).toBe(true); + }); + it('ignores status changes from servers outside the session baseline', async () => { const manager = new FakeMcpManager(); const lateClient = fakeMcpClient(); diff --git a/packages/agent-core-v2/test/agent/toolSelect/toolSelect.e2e.test.ts b/packages/agent-core-v2/test/agent/toolSelect/toolSelect.e2e.test.ts index 24e5749745d..6d57da21a2e 100644 --- a/packages/agent-core-v2/test/agent/toolSelect/toolSelect.e2e.test.ts +++ b/packages/agent-core-v2/test/agent/toolSelect/toolSelect.e2e.test.ts @@ -100,7 +100,9 @@ describe('progressive tool disclosure end-to-end', () => { await ctx.restorePersisted(); await ctx.rpc.setPermission({ mode: 'yolo' }); alpha = new StubMcpTool(MCP_ALPHA); - registration = ctx.get(IAgentToolRegistryService).register(alpha, { source: 'mcp' }); + registration = ctx + .get(IAgentToolRegistryService) + .register(alpha, { source: 'mcp', disclosure: 'deferred' }); }); afterEach(async () => { diff --git a/packages/agent-core-v2/test/agent/toolSelect/toolSelectService.test.ts b/packages/agent-core-v2/test/agent/toolSelect/toolSelectService.test.ts index f4e2c578c90..1f1bf197b0d 100644 --- a/packages/agent-core-v2/test/agent/toolSelect/toolSelectService.test.ts +++ b/packages/agent-core-v2/test/agent/toolSelect/toolSelectService.test.ts @@ -400,8 +400,12 @@ function createExecutorHarness(): ExecutorHarness { }; } -function registerMcp(h: Harness, tool: StubMcpTool): IDisposable { - const registration = h.registry.register(tool, { source: 'mcp' }); +function registerMcp( + h: Harness, + tool: StubMcpTool, + disclosure: ToolDisclosure = 'deferred', +): IDisposable { + const registration = h.registry.register(tool, { source: 'mcp', disclosure }); disposables.add(registration); return registration; } @@ -624,6 +628,21 @@ describe('AgentToolSelectService view shaping (gate open)', () => { expect(byName.get(SELECT_TOOLS_TOOL_NAME)?.deferred).toBeUndefined(); }); + it('keeps inline-disclosed MCP tools visible and out of the loadable manifest', () => { + const h = createHarness(); + registerMcp(h, new StubMcpTool(MCP_ALPHA), 'inline'); + registerMcp(h, new StubMcpTool(MCP_BETA)); + + const shaped = h.sut.shapeTools(h.registry.list()); + const byName = new Map(shaped.map((entry) => [entry.name, entry])); + expect(byName.get(MCP_ALPHA)?.deferred).toBeUndefined(); + expect(byName.has(MCP_BETA)).toBe(false); + + const announcement = h.sut.loadableToolsAnnouncement(); + expect(announcement).toContain(MCP_BETA); + expect(announcement).not.toContain(MCP_ALPHA); + }); + it('defers only opted-in user tools and restores them after selection', () => { const h = createHarness(); registerUser(h, new EchoTool(USER_DEFERRED), 'deferred'); @@ -1083,7 +1102,10 @@ describe('AgentToolSelectService loadable-tools announcements', () => { it('diffs registry additions and removals against the folded announcements', async () => { const h = createHarness(); registerMcp(h, new StubMcpTool(MCP_ALPHA)); - const betaRegistration = h.registry.register(new StubMcpTool(MCP_BETA), { source: 'mcp' }); + const betaRegistration = h.registry.register(new StubMcpTool(MCP_BETA), { + source: 'mcp', + disclosure: 'deferred', + }); disposables.add(betaRegistration); await announce(h); diff --git a/packages/agent-core-v2/test/mcpCore/connection-manager.test.ts b/packages/agent-core-v2/test/mcpCore/connection-manager.test.ts index bb32b85f80c..5e2d2e8e901 100644 --- a/packages/agent-core-v2/test/mcpCore/connection-manager.test.ts +++ b/packages/agent-core-v2/test/mcpCore/connection-manager.test.ts @@ -220,6 +220,20 @@ describe('McpConnectionManager', () => { } }, 15000); + it('reflects the deferred config field in the resolved view', async () => { + const cm = createManager(); + try { + await cm.connectAll({ + plain: stdioConfig(), + pinned: { ...stdioConfig(), deferred: false }, + }); + expect(cm.resolved('plain')?.deferred).toBe(true); + expect(cm.resolved('pinned')?.deferred).toBe(false); + } finally { + await cm.shutdown(); + } + }, 15000); + it('starts stdio servers in stdioCwd when config.cwd is omitted', async () => { const cwd = mkdtempSync(join(tmpdir(), 'kimi-mcp-manager-cwd-')); const cm = createManager({ stdioCwd: cwd }); From fc3b6584f20a2e671a75e0601f107e0f3bc64a3e Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Wed, 2 Sep 2026 14:05:45 +0800 Subject: [PATCH 3/3] feat(klient): mirror per-server deferred in the MCP config contract --- packages/klient/src/contract/mcp.ts | 1 + packages/klient/test/contract.test.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/packages/klient/src/contract/mcp.ts b/packages/klient/src/contract/mcp.ts index ed8827a7d23..674e0f05c08 100644 --- a/packages/klient/src/contract/mcp.ts +++ b/packages/klient/src/contract/mcp.ts @@ -13,6 +13,7 @@ export const mcpTimeoutMsSchema = z.number().int().min(1).max(2_147_483_647); const mcpServerCommonFields = { enabled: z.boolean().optional(), + deferred: z.boolean().optional(), startupTimeoutMs: mcpTimeoutMsSchema.optional(), toolTimeoutMs: mcpTimeoutMsSchema.optional(), enabledTools: z.array(z.string()).optional(), diff --git a/packages/klient/test/contract.test.ts b/packages/klient/test/contract.test.ts index 1571b6e68ff..3181e53afc0 100644 --- a/packages/klient/test/contract.test.ts +++ b/packages/klient/test/contract.test.ts @@ -57,6 +57,32 @@ describe('MCP timeout contract validation', () => { }); }); + it('session creation options preserve the per-server deferred field', () => { + const parsed = createSessionOptionsSchema.safeParse({ + workDir: '/tmp/example', + mcpServers: { + stdioExample: { transport: 'stdio', command: 'node', deferred: false }, + httpExample: { transport: 'http', url: 'https://example.com/mcp', deferred: true }, + sseExample: { transport: 'sse', url: 'https://example.com/sse' }, + }, + }); + expect(parsed.success).toBe(true); + expect(parsed.data?.mcpServers?.['stdioExample']).toEqual({ + transport: 'stdio', + command: 'node', + deferred: false, + }); + expect(parsed.data?.mcpServers?.['httpExample']).toEqual({ + transport: 'http', + url: 'https://example.com/mcp', + deferred: true, + }); + expect(parsed.data?.mcpServers?.['sseExample']).toEqual({ + transport: 'sse', + url: 'https://example.com/sse', + }); + }); + it('session creation options reject malformed mcpServers entries', () => { const parsed = createSessionOptionsSchema.safeParse({ workDir: '/tmp/example',