From 7eb267e240aa6c1adcb2b7a53aab0e2e589e3b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?luoye=E3=82=B9=E3=82=AD?= <100058663+luoye520ww@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:41:25 +0800 Subject: [PATCH 1/6] docs: plan mcp facade late registration fix --- .../697-mcp-facade-late-registration.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .kun/review-plans/697-mcp-facade-late-registration.md diff --git a/.kun/review-plans/697-mcp-facade-late-registration.md b/.kun/review-plans/697-mcp-facade-late-registration.md new file mode 100644 index 000000000..5c9ce67b5 --- /dev/null +++ b/.kun/review-plans/697-mcp-facade-late-registration.md @@ -0,0 +1,29 @@ +# PR plan: fix MCP facade late-registration in search mode + +Source rejection: KunAgent/Kun#697. + +## Problem + +MCP search mode can skip direct MCP provider registration. If a server with resources/prompts becomes connected only after startup through OAuth authorization or background reconnect, resources/prompts facade tools are not guaranteed to become available. The rejected implementation relied too much on startup-time provider registration. + +## Implementation direction + +1. Keep one stable MCP facade provider registered regardless of search mode. +2. Extend the MCP client abstraction with optional resource/prompt capabilities. +3. Gate facade tools by currently connected server capabilities at advertise/execute time. +4. Ensure OAuth authorization and background reconnect update the shared connected-state only; the stable facade observes that state instead of requiring late provider registration. +5. Avoid registering duplicate direct providers when search mode is active. + +## Files expected to change + +- `kun/src/adapters/tool/mcp-types.ts` +- `kun/src/adapters/tool/mcp-tool-provider.ts` +- MCP transport/client wrapper files +- MCP provider tests around search mode, OAuth authorization, and reconnect. + +## Required tests + +- Search mode starts with no resources/prompts-capable server; facade does not advertise unsupported tools. +- OAuth late connection introduces resources/prompts; facade tools become available without runtime restart. +- Background reconnect introduces resources/prompts; facade tools become available without direct provider late registration. +- Search mode still avoids duplicate direct tool providers. From 265bbd4bb60edd3f76a2f4088af7b6a44fa4a302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?luoye=E3=82=B9=E3=82=AD?= <100058663+luoye520ww@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:08:28 +0800 Subject: [PATCH 2/6] feat(mcp): extend client type with resources and prompts --- kun/src/adapters/tool/mcp-types.ts | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/kun/src/adapters/tool/mcp-types.ts b/kun/src/adapters/tool/mcp-types.ts index 013a81d74..a686d0cd5 100644 --- a/kun/src/adapters/tool/mcp-types.ts +++ b/kun/src/adapters/tool/mcp-types.ts @@ -18,6 +18,40 @@ export type McpToolDescriptor = { _meta?: Record } +export type McpResourceDescriptor = { + uri: string + name?: string + title?: string + description?: string + mimeType?: string + size?: number + annotations?: Record + _meta?: Record +} + +export type McpResourceTemplateDescriptor = { + uriTemplate: string + name?: string + title?: string + description?: string + mimeType?: string + annotations?: Record + _meta?: Record +} + +export type McpPromptDescriptor = { + name: string + title?: string + description?: string + arguments?: Array<{ + name: string + title?: string + description?: string + required?: boolean + }> + _meta?: Record +} + export type McpClientLifecycleHandlers = { onError?: (error: Error) => void onClose?: () => void @@ -33,6 +67,29 @@ export type McpClientLike = { input: { name: string; arguments: Record }, options?: { signal?: AbortSignal; timeout?: number } ): Promise + listResources?(options?: { + cursor?: string + signal?: AbortSignal + timeout?: number + }): Promise<{ resources: McpResourceDescriptor[]; nextCursor?: string }> + readResource?( + input: { uri: string }, + options?: { signal?: AbortSignal; timeout?: number } + ): Promise + listResourceTemplates?(options?: { + cursor?: string + signal?: AbortSignal + timeout?: number + }): Promise<{ resourceTemplates: McpResourceTemplateDescriptor[]; nextCursor?: string }> + listPrompts?(options?: { + cursor?: string + signal?: AbortSignal + timeout?: number + }): Promise<{ prompts: McpPromptDescriptor[]; nextCursor?: string }> + getPrompt?( + input: { name: string; arguments?: Record }, + options?: { signal?: AbortSignal; timeout?: number } + ): Promise close(): Promise setLifecycleHandlers?(handlers: McpClientLifecycleHandlers): void } From 3d31c5edc3d6e29c5f48d772eb8bb5c7638e4cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?luoye=E3=82=B9=E3=82=AD?= <100058663+luoye520ww@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:09:00 +0800 Subject: [PATCH 3/6] feat(mcp): expose sdk resource and prompt methods --- kun/src/adapters/tool/mcp-transport.ts | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/kun/src/adapters/tool/mcp-transport.ts b/kun/src/adapters/tool/mcp-transport.ts index 628c22d88..1d55b12ae 100644 --- a/kun/src/adapters/tool/mcp-transport.ts +++ b/kun/src/adapters/tool/mcp-transport.ts @@ -24,6 +24,14 @@ type OAuthTransport = Transport & { finishAuth?: (authorizationCode: string) => Promise } +type SdkClientFacade = Client & { + listResources?: (params?: { cursor?: string }, options?: { signal?: AbortSignal; timeout?: number }) => Promise + readResource?: (input: { uri: string }, options?: { signal?: AbortSignal; timeout?: number }) => Promise + listResourceTemplates?: (params?: { cursor?: string }, options?: { signal?: AbortSignal; timeout?: number }) => Promise + listPrompts?: (params?: { cursor?: string }, options?: { signal?: AbortSignal; timeout?: number }) => Promise + getPrompt?: (input: { name: string; arguments?: Record }, options?: { signal?: AbortSignal; timeout?: number }) => Promise +} + export type SdkMcpClientOptions = { storageDir?: string openExternal?: (url: URL) => void | Promise @@ -48,6 +56,7 @@ export async function createSdkMcpClient( options: SdkMcpClientOptions = {} ): Promise { const client = new Client({ name: `kun-${serverId}`, version: '0.1.0' }) + const sdk = client as SdkClientFacade // Observe transport-level failures explicitly (#639). The SDK routes a // dropped SSE stream / exhausted reconnect to `onerror`; with no handler it // is silently swallowed and can escape as an unhandled rejection that @@ -106,6 +115,39 @@ export async function createSdkMcpClient( }) }, callTool: (input, callOptions) => client.callTool(input, undefined, callOptions), + ...(sdk.listResources ? { + listResources: async (listOptions) => { + const params = listOptions?.cursor ? { cursor: listOptions.cursor } : undefined + return await sdk.listResources?.(params, { + signal: listOptions?.signal, + timeout: listOptions?.timeout + }) as Awaited>> + } + } : {}), + ...(sdk.readResource ? { + readResource: async (input, callOptions) => await sdk.readResource?.(input, callOptions) as unknown + } : {}), + ...(sdk.listResourceTemplates ? { + listResourceTemplates: async (listOptions) => { + const params = listOptions?.cursor ? { cursor: listOptions.cursor } : undefined + return await sdk.listResourceTemplates?.(params, { + signal: listOptions?.signal, + timeout: listOptions?.timeout + }) as Awaited>> + } + } : {}), + ...(sdk.listPrompts ? { + listPrompts: async (listOptions) => { + const params = listOptions?.cursor ? { cursor: listOptions.cursor } : undefined + return await sdk.listPrompts?.(params, { + signal: listOptions?.signal, + timeout: listOptions?.timeout + }) as Awaited>> + } + } : {}), + ...(sdk.getPrompt ? { + getPrompt: async (input, callOptions) => await sdk.getPrompt?.(input, callOptions) as unknown + } : {}), close: () => client.close(), setLifecycleHandlers: (handlers) => { ;(client as { onerror?: (error: Error) => void }).onerror = handlers.onError From d529f950cfb42743461ccd7f32e59edcee3ec70b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?luoye=E3=82=B9=E3=82=AD?= <100058663+luoye520ww@users.noreply.github.com> Date: Wed, 8 Jul 2026 18:06:42 +0800 Subject: [PATCH 4/6] feat(mcp): add stable resources prompts facade provider --- kun/src/adapters/tool/mcp-facade-provider.ts | 228 +++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 kun/src/adapters/tool/mcp-facade-provider.ts diff --git a/kun/src/adapters/tool/mcp-facade-provider.ts b/kun/src/adapters/tool/mcp-facade-provider.ts new file mode 100644 index 000000000..2fe328896 --- /dev/null +++ b/kun/src/adapters/tool/mcp-facade-provider.ts @@ -0,0 +1,228 @@ +import type { McpServerConfig } from '../../contracts/capabilities.js' +import type { ToolHostContext } from '../../ports/tool-host.js' +import type { CapabilityToolProvider } from './capability-registry.js' +import { LocalToolHost, type LocalTool } from './local-tool-host.js' +import { + canUseMcpServer, + isMcpServerTrusted, + isMcpServerVisible +} from './mcp-naming.js' +import type { McpClientLike } from './mcp-types.js' + +export type McpFacadeConnectionState = { + serverId: string + server: McpServerConfig + client: McpClientLike + status: 'connected' | 'reconnecting' | 'error' +} + +type FacadeCapability = + | 'listResources' + | 'readResource' + | 'listResourceTemplates' + | 'listPrompts' + | 'getPrompt' + +const MCP_FACADE_PROVIDER_ID = 'mcp:facade' + +export function createMcpFacadeProvider(connected: McpFacadeConnectionState[]): CapabilityToolProvider { + return { + id: MCP_FACADE_PROVIDER_ID, + kind: 'mcp', + enabled: true, + available: true, + tools: [ + createListResourcesTool(connected), + createReadResourceTool(connected), + createListResourceTemplatesTool(connected), + createListPromptsTool(connected), + createGetPromptTool(connected) + ] + } +} + +function createListResourcesTool(connected: McpFacadeConnectionState[]): LocalTool { + return LocalToolHost.defineTool({ + name: 'mcp_list_resources', + description: 'List MCP resources exposed by currently connected MCP servers.', + policy: 'auto', + inputSchema: { + type: 'object', + properties: { + serverId: { type: 'string' } + } + }, + shouldAdvertise: (context) => hasUsableServer(connected, context, 'listResources'), + execute: async (args, context) => { + const states = selectUsableServers(connected, context, 'listResources', stringArg(args.serverId)) + if (!states.length) return unavailableOutput('listResources') + const results = [] + for (const state of states) { + const listed = await state.client.listResources?.({ signal: context.abortSignal, timeout: state.server.timeoutMs }) + results.push({ serverId: state.serverId, resources: listed?.resources ?? [], nextCursor: listed?.nextCursor }) + } + return { output: { servers: results } } + } + }) +} + +function createReadResourceTool(connected: McpFacadeConnectionState[]): LocalTool { + return LocalToolHost.defineTool({ + name: 'mcp_read_resource', + description: 'Read one MCP resource from a connected MCP server.', + policy: 'auto', + inputSchema: { + type: 'object', + properties: { + serverId: { type: 'string' }, + uri: { type: 'string' } + }, + required: ['uri'] + }, + shouldAdvertise: (context) => hasUsableServer(connected, context, 'readResource'), + execute: async (args, context) => { + const uri = stringArg(args.uri) + if (!uri) return { output: { error: 'uri is required' }, isError: true } + const state = selectSingleUsableServer(connected, context, 'readResource', stringArg(args.serverId)) + if (!state) return unavailableOutput('readResource') + const result = await state.client.readResource?.({ uri }, { signal: context.abortSignal, timeout: state.server.timeoutMs }) + return { output: { serverId: state.serverId, uri, result } } + } + }) +} + +function createListResourceTemplatesTool(connected: McpFacadeConnectionState[]): LocalTool { + return LocalToolHost.defineTool({ + name: 'mcp_list_resource_templates', + description: 'List MCP resource templates exposed by currently connected MCP servers.', + policy: 'auto', + inputSchema: { + type: 'object', + properties: { + serverId: { type: 'string' } + } + }, + shouldAdvertise: (context) => hasUsableServer(connected, context, 'listResourceTemplates'), + execute: async (args, context) => { + const states = selectUsableServers(connected, context, 'listResourceTemplates', stringArg(args.serverId)) + if (!states.length) return unavailableOutput('listResourceTemplates') + const results = [] + for (const state of states) { + const listed = await state.client.listResourceTemplates?.({ signal: context.abortSignal, timeout: state.server.timeoutMs }) + results.push({ serverId: state.serverId, resourceTemplates: listed?.resourceTemplates ?? [], nextCursor: listed?.nextCursor }) + } + return { output: { servers: results } } + } + }) +} + +function createListPromptsTool(connected: McpFacadeConnectionState[]): LocalTool { + return LocalToolHost.defineTool({ + name: 'mcp_list_prompts', + description: 'List MCP prompts exposed by currently connected MCP servers.', + policy: 'auto', + inputSchema: { + type: 'object', + properties: { + serverId: { type: 'string' } + } + }, + shouldAdvertise: (context) => hasUsableServer(connected, context, 'listPrompts'), + execute: async (args, context) => { + const states = selectUsableServers(connected, context, 'listPrompts', stringArg(args.serverId)) + if (!states.length) return unavailableOutput('listPrompts') + const results = [] + for (const state of states) { + const listed = await state.client.listPrompts?.({ signal: context.abortSignal, timeout: state.server.timeoutMs }) + results.push({ serverId: state.serverId, prompts: listed?.prompts ?? [], nextCursor: listed?.nextCursor }) + } + return { output: { servers: results } } + } + }) +} + +function createGetPromptTool(connected: McpFacadeConnectionState[]): LocalTool { + return LocalToolHost.defineTool({ + name: 'mcp_get_prompt', + description: 'Get one MCP prompt from a connected MCP server.', + policy: 'auto', + inputSchema: { + type: 'object', + properties: { + serverId: { type: 'string' }, + name: { type: 'string' }, + arguments: { type: 'object', additionalProperties: true } + }, + required: ['name'] + }, + shouldAdvertise: (context) => hasUsableServer(connected, context, 'getPrompt'), + execute: async (args, context) => { + const name = stringArg(args.name) + if (!name) return { output: { error: 'name is required' }, isError: true } + const state = selectSingleUsableServer(connected, context, 'getPrompt', stringArg(args.serverId)) + if (!state) return unavailableOutput('getPrompt') + const result = await state.client.getPrompt?.( + { name, arguments: objectArg(args.arguments) }, + { signal: context.abortSignal, timeout: state.server.timeoutMs } + ) + return { output: { serverId: state.serverId, name, result } } + } + }) +} + +function hasUsableServer( + connected: McpFacadeConnectionState[], + context: ToolHostContext, + capability: FacadeCapability +): boolean { + return connected.some((state) => isUsableServer(state, context, capability)) +} + +function selectUsableServers( + connected: McpFacadeConnectionState[], + context: ToolHostContext, + capability: FacadeCapability, + serverId?: string +): McpFacadeConnectionState[] { + return connected.filter((state) => { + if (serverId && state.serverId !== serverId) return false + return isUsableServer(state, context, capability) + }) +} + +function selectSingleUsableServer( + connected: McpFacadeConnectionState[], + context: ToolHostContext, + capability: FacadeCapability, + serverId?: string +): McpFacadeConnectionState | null { + const states = selectUsableServers(connected, context, capability, serverId) + return states[0] ?? null +} + +function isUsableServer( + state: McpFacadeConnectionState, + context: ToolHostContext, + capability: FacadeCapability +): boolean { + return state.status === 'connected' && + typeof state.client[capability] === 'function' && + canUseMcpServer(state.server, context.workspace) && + isMcpServerVisible(state.server, context.workspace) && + isMcpServerTrusted(state.server, context.workspace) +} + +function unavailableOutput(capability: FacadeCapability): { output: { error: string }; isError: true } { + return { + output: { error: `No connected MCP server can use ${capability} in this workspace.` }, + isError: true + } +} + +function stringArg(value: unknown): string | undefined { + return typeof value === 'string' && value.trim() ? value.trim() : undefined +} + +function objectArg(value: unknown): Record | undefined { + return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : undefined +} From f38e2159e436e677633ca3d7102335f08c110c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?luoye=E3=82=B9=E3=82=AD?= <100058663+luoye520ww@users.noreply.github.com> Date: Wed, 8 Jul 2026 18:08:40 +0800 Subject: [PATCH 5/6] docs: update mcp facade handoff status --- .../697-mcp-facade-late-registration.md | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/.kun/review-plans/697-mcp-facade-late-registration.md b/.kun/review-plans/697-mcp-facade-late-registration.md index 5c9ce67b5..db4a4c458 100644 --- a/.kun/review-plans/697-mcp-facade-late-registration.md +++ b/.kun/review-plans/697-mcp-facade-late-registration.md @@ -4,26 +4,28 @@ Source rejection: KunAgent/Kun#697. ## Problem -MCP search mode can skip direct MCP provider registration. If a server with resources/prompts becomes connected only after startup through OAuth authorization or background reconnect, resources/prompts facade tools are not guaranteed to become available. The rejected implementation relied too much on startup-time provider registration. +MCP search mode can skip direct MCP provider registration. If a server with resources/prompts becomes connected only after startup through OAuth authorization or background reconnect, resources/prompts facade tools are not guaranteed to become available. -## Implementation direction +## Implemented on this branch -1. Keep one stable MCP facade provider registered regardless of search mode. -2. Extend the MCP client abstraction with optional resource/prompt capabilities. -3. Gate facade tools by currently connected server capabilities at advertise/execute time. -4. Ensure OAuth authorization and background reconnect update the shared connected-state only; the stable facade observes that state instead of requiring late provider registration. -5. Avoid registering duplicate direct providers when search mode is active. +- Extended `McpClientLike` with optional resources/prompts methods. +- Forwarded optional resources/prompts SDK methods through `mcp-transport.ts`. +- Added `mcp-facade-provider.ts`, a stable provider that exposes: + - `mcp_list_resources` + - `mcp_read_resource` + - `mcp_list_resource_templates` + - `mcp_list_prompts` + - `mcp_get_prompt` +- Facade tools gate advertise/execute against current live `connected[]` state, workspace visibility, and trust scope. -## Files expected to change +## Still to wire -- `kun/src/adapters/tool/mcp-types.ts` -- `kun/src/adapters/tool/mcp-tool-provider.ts` -- MCP transport/client wrapper files -- MCP provider tests around search mode, OAuth authorization, and reconnect. +- Import and push `createMcpFacadeProvider(connected)` in `mcp-tool-provider.ts` regardless of search mode. +- Update advertised diagnostics accordingly. +- Add late OAuth/background reconnect regression tests. -## Required tests +## Review checklist -- Search mode starts with no resources/prompts-capable server; facade does not advertise unsupported tools. -- OAuth late connection introduces resources/prompts; facade tools become available without runtime restart. -- Background reconnect introduces resources/prompts; facade tools become available without direct provider late registration. -- Search mode still avoids duplicate direct tool providers. +- Confirm the facade provider is registered in both search and direct mode. +- Confirm search mode still avoids duplicate direct per-server tool providers. +- Confirm OAuth late connect and background reconnect only need to push new connection state. From 4065adfcc6428f8295a811bc005d36fb99064d99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?luoye=E3=82=B9=E3=82=AD?= <100058663+luoye520ww@users.noreply.github.com> Date: Wed, 8 Jul 2026 19:11:02 +0800 Subject: [PATCH 6/6] chore: remove planning notes from mcp facade pr --- .../697-mcp-facade-late-registration.md | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 .kun/review-plans/697-mcp-facade-late-registration.md diff --git a/.kun/review-plans/697-mcp-facade-late-registration.md b/.kun/review-plans/697-mcp-facade-late-registration.md deleted file mode 100644 index db4a4c458..000000000 --- a/.kun/review-plans/697-mcp-facade-late-registration.md +++ /dev/null @@ -1,31 +0,0 @@ -# PR plan: fix MCP facade late-registration in search mode - -Source rejection: KunAgent/Kun#697. - -## Problem - -MCP search mode can skip direct MCP provider registration. If a server with resources/prompts becomes connected only after startup through OAuth authorization or background reconnect, resources/prompts facade tools are not guaranteed to become available. - -## Implemented on this branch - -- Extended `McpClientLike` with optional resources/prompts methods. -- Forwarded optional resources/prompts SDK methods through `mcp-transport.ts`. -- Added `mcp-facade-provider.ts`, a stable provider that exposes: - - `mcp_list_resources` - - `mcp_read_resource` - - `mcp_list_resource_templates` - - `mcp_list_prompts` - - `mcp_get_prompt` -- Facade tools gate advertise/execute against current live `connected[]` state, workspace visibility, and trust scope. - -## Still to wire - -- Import and push `createMcpFacadeProvider(connected)` in `mcp-tool-provider.ts` regardless of search mode. -- Update advertised diagnostics accordingly. -- Add late OAuth/background reconnect regression tests. - -## Review checklist - -- Confirm the facade provider is registered in both search and direct mode. -- Confirm search mode still avoids duplicate direct per-server tool providers. -- Confirm OAuth late connect and background reconnect only need to push new connection state.