fix(repository-plugin): reject incomplete MCP publication

This commit is contained in:
Tianyi Cui
2026-08-09 00:50:54 +08:00
parent 29b3e3fa84
commit cc7bd4948d
20 changed files with 89 additions and 43 deletions

View File

@@ -72,7 +72,7 @@ export interface StdioConfig {
cwd: string
/** Per-tool-call timeout in milliseconds. */
toolCallTimeoutMs: number
/** Fail plugin activation when the initial connection or tool discovery fails. */
/** Fail plugin activation when the initial connection or tool synchronization fails. */
failOnStartupError: boolean
}
@@ -92,7 +92,7 @@ export interface StreamableHttpConfig {
headers: Record<string, string>
/** Per-tool-call timeout in milliseconds. */
toolCallTimeoutMs: number
/** Fail plugin activation when the initial connection or tool discovery fails. */
/** Fail plugin activation when the initial connection or tool synchronization fails. */
failOnStartupError: boolean
}
@@ -155,6 +155,7 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
)
const opts = {
registrationFailure: 'contain' as const,
serverName: config.serverName,
toolCallTimeoutMs: config.toolCallTimeoutMs,
}
@@ -166,7 +167,10 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
const ready = (async () => {
await client.connect(transport)
let disposers = await syncTools(client, ctx, opts, new Map())
let disposers = await syncTools(client, ctx, {
...opts,
registrationFailure: config.failOnStartupError ? 'throw' : 'contain',
}, new Map())
client.setNotificationHandler(
ToolListChangedNotificationSchema,
@@ -184,7 +188,7 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
return { getDisposers: () => disposers }
})().catch((error: unknown) => {
ctx.logger.error(`mcp-client(${config.serverName}): failed to connect: ${String(error)}`)
ctx.logger.error(`mcp-client(${config.serverName}): startup failed: ${String(error)}`)
return { getDisposers: () => new Map<string, () => void>(), error }
})
@@ -196,6 +200,6 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
const outcome = await ready
if ('error' in outcome && config.failOnStartupError) {
throw new Error(`mcp-client(${config.serverName}): initial connection or tool discovery failed`, { cause: outcome.error })
throw new Error(`mcp-client(${config.serverName}): initial connection or tool synchronization failed`, { cause: outcome.error })
}
}

View File

@@ -23,6 +23,8 @@ import type { JsonSchemaNode, JsonValue } from '@deepseek-ai/dsh-tools'
/** Resolved options relevant to tool bridging. */
export interface ToolBridgeOptions {
/** Whether a registry conflict is contained or rejects this synchronization. */
registrationFailure: 'contain' | 'throw'
serverName: string
toolCallTimeoutMs: number
}
@@ -111,8 +113,9 @@ export function publicToolName(serverName: string, rawName: string): string {
* 2. Swap: dispose the previous generation, register the new one. A registry
* conflict here can only mean a foreign registration squats on this
* server's `mcp__<serverName>__` namespace — the partial generation is
* rolled back (zero tools from this server), the error is logged, and an
* empty map is returned.
* rolled back (zero tools from this server) and logged. Initial strict
* synchronization may propagate the conflict so its parent transaction
* rejects; ordinary clients and later re-syncs return an empty map.
*
* @param client - Connected MCP Client instance used to list and call tools.
* @param ctx - Cordis context providing the `tools` service for registration.
@@ -164,6 +167,7 @@ export async function syncTools(
// sees either the full generation or none of it — never a partial set.
for (const dispose of disposers.values()) dispose()
ctx.logger.error(`mcp-client(${opts.serverName}): tool registration failed, no tools registered: ${String(error)}`)
if (opts.registrationFailure === 'throw') throw error
return new Map()
}
return disposers