fix(mcp-client): bind strict sync to activation

The supervisor selected strict startup registration with a shared isFirstSync flag. Because the MCP SDK may deliver tools/list_changed before connect() resolves, that notification could enter the sync queue first, consume the strict option inside its contained handler, and leave the actual activation sync non-fatal.

Pass startup intent explicitly to connectGeneration(). Only the plugin activation attempt receives the failOnStartupError registration policy; notification-driven syncs and later reconnect generations always use contained runtime semantics. Queue arrival order can no longer redefine startup behavior.

A regression test injects list_changed from inside connect(), keeps a foreign namespace squatter in place, and proves activation still rejects after the notification's contained sync. Focused package coverage remains 100%, and the bilingual reconnect note records the ownership rule.
This commit is contained in:
Tianyi Cui
2026-08-11 00:00:27 +08:00
parent 6147f02386
commit 442f0ef839
5 changed files with 43 additions and 16 deletions

View File

@@ -288,6 +288,32 @@ describe('apply (plugin lifecycle)', () => {
expect(mockClose).toHaveBeenCalled()
})
it('preserves strict startup registration when list_changed arrives before connect resolves', async () => {
ctx.tools.register({
name: 'mcp__srv__remote',
description: 'Foreign squatter',
parameters: { type: 'object' },
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value as string }],
},
execute: async () => 'foreign',
})
mockConnect.mockImplementation(async () => {
const handler = mockSetNotificationHandler.mock.calls[0]![1] as () => Promise<void>
await handler()
})
await expect(apply(ctx, {
...stdioConfig,
failOnStartupError: true,
})).rejects.toThrow('initial connection or tool synchronization failed')
expect(mockListTools).toHaveBeenCalledTimes(2)
expect(ctx.tools.get('mcp__srv__remote')?.description).toBe('Foreign squatter')
await ctx.fiber.dispose()
})
it('re-syncs tools on ToolListChanged notification', async () => {
await apply(ctx, stdioConfig)