Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/dynamic-tools-official-models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Add the dynamically_loaded_tools capability to official Kimi Code models when the service declares support for message-level tool declarations.
5 changes: 5 additions & 0 deletions .changeset/mcp-server-deferred-config.md
Original file line number Diff line number Diff line change
@@ -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`.
5 changes: 5 additions & 0 deletions .changeset/select-tools-profile-activation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix the select_tools tool never being registered because agent profiles do not list it in their tool allowlists.
1 change: 1 addition & 0 deletions docs/en/customization/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Optional fields:
| `headers` | `Record<string, string>` | 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 |
Expand Down
5 changes: 3 additions & 2 deletions docs/zh/customization/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ MCP server 配置写在 `mcp.json` 中,分两层:
| `headers` | `Record<string, string>` | HTTP、SSE | 附加到每次请求的静态请求头 |
| `bearerTokenEnvVar` | `string` | HTTP、SSE | 存放 bearer token 的环境变量名 |
| `enabled` | `boolean` | 全部 | 设为 `false` 可禁用该 server |
| `startupTimeoutMs` | `number` | 全部 | 连接超时,默认 `30000` 毫秒 |
| `toolTimeoutMs` | `number` | 全部 | 单次工具调用超时(毫秒) |
| `deferred` | `boolean` | 全部 | 实验功能:启用 `tool-select` 标志且模型声明动态工具加载能力时,该 server 的工具默认不进入顶层工具列表,由模型通过 `select_tools` 按需加载;设为 `false` 则始终直接暴露。默认 `true`;标志未启用时该字段无效 |
| `startupTimeoutMs` | `number` | 全部 | 连接超时,取值范围为 `1` 到 `2147483647` 毫秒,默认 `30000` |
| `toolTimeoutMs` | `number` | 全部 | 单次工具调用超时,取值范围为 `1` 到 `2147483647` 毫秒 |
| `enabledTools` | `string[]` | 全部 | 工具白名单 |
| `disabledTools` | `string[]` | 全部 | 工具黑名单 |

Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/src/agent/mcp/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface McpResolvedServer {
readonly tools: readonly KosongTool[];
readonly rawTools: readonly MCPToolDefinition[];
readonly enabledNames: ReadonlySet<string>;
readonly deferred: boolean;
}

export interface IAgentMcpService {
Expand Down
12 changes: 10 additions & 2 deletions packages/agent-core-v2/src/agent/mcp/mcpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,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);
Expand All @@ -235,7 +236,13 @@ 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 deferred = this.mcpHandle.connectionManager.configOf(entry.name)?.deferred !== false;
const disposable = this._register(
this.registry.register(tool, {
source: 'mcp',
disclosure: deferred ? 'deferred' : 'inline',
}),
);
this.mcpTools.set(tool.name, { disposable, serverName: entry.name });
this.mcpToolsByServer.set(entry.name, [tool.name]);
void this.dispatcher.dispatch(
Expand All @@ -252,6 +259,7 @@ export class AgentMcpService extends Service implements IAgentMcpService {
client: MCPClient,
tools: readonly KosongTool[],
enabledTools: ReadonlySet<string>,
deferred: boolean,
): {
readonly registered: readonly string[];
readonly collisions: readonly McpToolCollision[];
Expand Down Expand Up @@ -292,7 +300,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 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { IEventBus } from '#/app/event/eventBus';
import { IAgentProfileService } from '#/agent/profile/profile';
import { AgentStatusUpdated } from '#/agent/usage/usageEvents';
import { isToolActive } from '#/agent/toolPolicy/evaluate';
import { SELECT_TOOLS_TOOL_NAME } from '#/agent/toolSelect/toolSelect';
import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry';
import { AgentToolContribution } from '#/agent/toolRegistry/toolContribution';
import { ISessionToolPolicyGate } from '#/session/sessionToolPolicyGate/sessionToolPolicyGate';
Expand Down Expand Up @@ -55,6 +56,7 @@ export class AgentToolActivationService extends Service implements IAgentToolAct
if (records.length === 0) return;
const data = this.profile.data();
const policy = { tools: data.activeToolNames, disallowedTools: data.disallowedTools };
const disclosurePolicy = { disallowedTools: data.disallowedTools };
const workspaceVeto = { disallowedTools: this.toolPolicyGate.disabledTools };
this.instantiationService.invokeFunction((accessor) => {
for (const record of records) {
Expand All @@ -63,7 +65,11 @@ export class AgentToolActivationService extends Service implements IAgentToolAct
if (this.toolRegistry.resolve(options.name) !== undefined) continue;
if (!this.runtimeAllows(record)) continue;
if (!isToolActive(workspaceVeto, options.name, source)) continue;
if (!isToolActive(policy, options.name, source)) continue;
const activeByProfile =
options.name === SELECT_TOOLS_TOOL_NAME
? isToolActive(disclosurePolicy, options.name, source)
: isToolActive(policy, options.name, source);
if (!activeByProfile) continue;
if (options.when !== undefined && !options.when(accessor)) continue;
const tool = accessor.get(id);
const registration = this.toolRegistry.register(tool, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[] {
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core-v2/src/mcpCore/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
4 changes: 4 additions & 0 deletions packages/agent-core-v2/src/mcpCore/connection-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface McpConnectionView {
readonly oauthService: McpOAuthService | undefined;
list(): readonly McpServerEntry[];
get(name: string): McpServerEntry | undefined;
configOf(name: string): McpServerConfig | undefined;
resolved(
name: string,
):
Expand All @@ -49,6 +50,7 @@ export interface McpConnectionView {
tools: readonly Tool[];
rawTools: readonly MCPToolDefinition[];
enabledNames: ReadonlySet<string>;
deferred: boolean;
}
| undefined;
getRemoteServerUrl(name: string): string | undefined;
Expand Down Expand Up @@ -144,6 +146,7 @@ export class McpConnectionManager implements McpConnectionView {
tools: readonly Tool[];
rawTools: readonly MCPToolDefinition[];
enabledNames: ReadonlySet<string>;
deferred: boolean;
}
| undefined {
const entry = this.entries.get(name);
Expand All @@ -160,6 +163,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,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
McpServerEntry,
McpStatusListener,
} from '#/mcpCore/connection-manager';
import type { McpServerConfig } from '#/mcpCore/config-schema';
import type { McpOAuthService } from '#/mcpCore/oauth/service';
import { abortable } from '#/_base/utils/abort';

Expand All @@ -27,6 +28,10 @@ export class MergedMcpConnectionView implements McpConnectionView {
return this.owner(name).get(name);
}

configOf(name: string): McpServerConfig | undefined {
return this.owner(name).configOf(name);
}

resolved(name: string): ReturnType<McpConnectionView['resolved']> {
return this.owner(name).resolved(name);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2114,7 +2114,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',
Expand Down
34 changes: 31 additions & 3 deletions packages/agent-core-v2/test/agent/mcp/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { Event2 } from '#/app/event/event2';
import { IEventBus } from '#/app/event/eventBus';
import { ITelemetryService } from '#/app/telemetry/telemetry';
import type { McpConnectionManager, McpServerEntry } from '#/mcpCore/connection-manager';
import type { McpServerConfig } from '#/mcpCore/config-schema';
import { IAgentMcpService } from '#/agent/mcp/mcp';
import { renderToolResultForModel } from '#/agent/contextMemory/toolResultRender';
import { AgentMcpService } from '#/agent/mcp/mcpService';
Expand Down Expand Up @@ -57,10 +58,12 @@ interface ResolvedServer {
readonly tools: readonly KosongTool[];
readonly rawTools: readonly MCPToolDefinition[];
readonly enabledNames: ReadonlySet<string>;
readonly deferred: boolean;
}

class FakeMcpManager {
private readonly entries = new Map<string, McpServerEntry>();
private readonly configs = new Map<string, McpServerConfig>();
private readonly resolvedEntries = new Map<string, ResolvedServer>();
private readonly listeners = new Set<(entry: McpServerEntry) => void>();
readonly oauthService: McpOAuthService | undefined;
Expand All @@ -77,6 +80,10 @@ class FakeMcpManager {
return this.entries.get(name);
}

configOf(name: string): McpServerConfig | undefined {
return this.configs.get(name);
}

resolved(name: string): ResolvedServer | undefined {
if (this.entries.get(name)?.status !== 'connected') return undefined;
return this.resolvedEntries.get(name);
Expand Down Expand Up @@ -125,6 +132,7 @@ class FakeMcpManager {
tools: readonly KosongTool[],
enabledNames = new Set(tools.map((tool) => tool.name)),
rawTools?: readonly MCPToolDefinition[],
deferred = true,
): void {
const resolvedRawTools =
rawTools ??
Expand All @@ -138,6 +146,7 @@ class FakeMcpManager {
tools,
rawTools: resolvedRawTools,
enabledNames,
deferred,
});
}

Expand All @@ -153,7 +162,10 @@ class FakeMcpManager {
this.emit(entry);
}

needsAuth(name = 'needs-auth'): void {
needsAuth(name = 'needs-auth', options: { readonly deferred?: boolean } = {}): void {
if (options.deferred !== undefined) {
this.configs.set(name, { deferred: options.deferred } as unknown as McpServerConfig);
}
const entry: McpServerEntry = {
name,
transport: 'http',
Expand Down Expand Up @@ -321,6 +333,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',
Expand Down Expand Up @@ -364,6 +377,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();
Expand Down Expand Up @@ -1223,7 +1249,7 @@ describe('AgentMcpService', () => {
expect(receivedSignal).toBe(controller.signal);
});

it('registers a synthetic authenticate tool when a server needs auth', () => {
it('registers a synthetic authenticate tool inline when the server declares deferred: false', () => {
const oauthService = {
beginAuthorization: async () => ({
authorizationUrl: new URL('https://example.com/authorize'),
Expand All @@ -1234,13 +1260,14 @@ describe('AgentMcpService', () => {
const manager = new FakeMcpManager({ oauthService });
createService(manager);

manager.needsAuth();
manager.needsAuth('needs-auth', { deferred: false });

const tools = ix.get(IAgentToolRegistryService).list();
expect(tools).toEqual([
expect.objectContaining({
name: 'mcp__needs-auth__authenticate',
source: 'mcp',
disclosure: 'inline',
}),
]);
});
Expand All @@ -1263,6 +1290,7 @@ describe('AgentMcpService', () => {
expect.objectContaining({
name: 'mcp__needs-auth__authenticate',
source: 'mcp',
disclosure: 'deferred',
}),
]);
expect(events).toContainEqual(
Expand Down
Loading
Loading