fix(mcp-client): await Cordis startup discovery
This commit is contained in:
@@ -82,6 +82,7 @@ const stdioConfig: Config = {
|
||||
env: {},
|
||||
cwd: '',
|
||||
toolCallTimeoutMs: 60_000,
|
||||
failOnStartupError: false,
|
||||
}
|
||||
|
||||
// ---- Tests ----
|
||||
@@ -150,11 +151,30 @@ describe('apply (plugin lifecycle)', () => {
|
||||
expect(ctx.tools.get('remote')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps the Cordis plugin loading until initial discovery publishes its tools', async () => {
|
||||
const connection: PromiseWithResolvers<void> = Promise.withResolvers()
|
||||
mockConnect.mockImplementation(async () => {
|
||||
await connection.promise
|
||||
})
|
||||
const fiber = ctx.plugin({ name: 'mcp-client-lifecycle', inject, apply }, stdioConfig)
|
||||
let activated = false
|
||||
const activation = Promise.resolve(fiber).then(() => { activated = true })
|
||||
|
||||
await vi.waitFor(() => { expect(mockConnect).toHaveBeenCalled() })
|
||||
expect(activated).toBe(false)
|
||||
expect(ctx.tools.get('mcp__srv__remote')).toBeUndefined()
|
||||
|
||||
connection.resolve()
|
||||
await activation
|
||||
expect(ctx.tools.get('mcp__srv__remote')).toBeDefined()
|
||||
await fiber.dispose()
|
||||
})
|
||||
|
||||
it('rejects a duplicate serverName at load and leaves the first instance intact', async () => {
|
||||
await apply(ctx, stdioConfig)
|
||||
expect(ctx.tools.get('mcp__srv__remote')).toBeDefined()
|
||||
|
||||
expect(() => { void apply(ctx, stdioConfig) }).toThrow(/serverName "srv" is already in use/)
|
||||
await expect(apply(ctx, stdioConfig)).rejects.toThrow(/serverName "srv" is already in use/)
|
||||
// First instance unaffected.
|
||||
expect(ctx.tools.get('mcp__srv__remote')).toBeDefined()
|
||||
})
|
||||
@@ -204,6 +224,19 @@ describe('apply (plugin lifecycle)', () => {
|
||||
expect(mockClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects activation and still closes the client when startup failure is configured as fatal', async () => {
|
||||
mockConnect.mockRejectedValue(new Error('connection refused'))
|
||||
await expect(apply(ctx, {
|
||||
...stdioConfig,
|
||||
failOnStartupError: true,
|
||||
})).rejects.toThrow('initial connection or tool discovery failed')
|
||||
|
||||
expect(mockListTools).not.toHaveBeenCalled()
|
||||
expect(ctx.tools.get('mcp__srv__remote')).toBeUndefined()
|
||||
await ctx.fiber.dispose()
|
||||
expect(mockClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('re-syncs tools on ToolListChanged notification', async () => {
|
||||
await apply(ctx, stdioConfig)
|
||||
|
||||
@@ -275,6 +308,7 @@ describe('apply (plugin lifecycle)', () => {
|
||||
url: 'http://localhost:3000/mcp',
|
||||
headers: { Authorization: 'Bearer x' },
|
||||
toolCallTimeoutMs: 30_000,
|
||||
failOnStartupError: false,
|
||||
}
|
||||
|
||||
await apply(ctx, httpConfig)
|
||||
|
||||
@@ -75,6 +75,7 @@ describe('fixture server — controlled scenarios', () => {
|
||||
env: {},
|
||||
cwd: packageDir,
|
||||
toolCallTimeoutMs: 15_000,
|
||||
failOnStartupError: false,
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -164,10 +165,11 @@ describe('fixture server — duplicate serverName', () => {
|
||||
env: {},
|
||||
cwd: packageDir,
|
||||
toolCallTimeoutMs: 15_000,
|
||||
failOnStartupError: false,
|
||||
}
|
||||
await apply(ctx, config)
|
||||
|
||||
expect(() => { void apply(ctx, config) }).toThrow(/serverName "dup" is already in use/)
|
||||
await expect(apply(ctx, config)).rejects.toThrow(/serverName "dup" is already in use/)
|
||||
|
||||
await ctx.fiber.dispose()
|
||||
await sleep(200)
|
||||
@@ -185,6 +187,7 @@ describe('fixture server — disposal', () => {
|
||||
env: {},
|
||||
cwd: packageDir,
|
||||
toolCallTimeoutMs: 15_000,
|
||||
failOnStartupError: false,
|
||||
})
|
||||
|
||||
// Tools are registered before dispose.
|
||||
@@ -210,6 +213,7 @@ describe('server-everything — official test server', () => {
|
||||
env: {},
|
||||
cwd: '',
|
||||
toolCallTimeoutMs: 30_000,
|
||||
failOnStartupError: false,
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -277,6 +281,7 @@ describe('server-filesystem — real filesystem operations', () => {
|
||||
env: {},
|
||||
cwd: '',
|
||||
toolCallTimeoutMs: 30_000,
|
||||
failOnStartupError: false,
|
||||
}
|
||||
await apply(ctx, config)
|
||||
}, 60_000)
|
||||
@@ -393,6 +398,7 @@ describe('streamable-http — in-process MCP server', () => {
|
||||
url: baseUrl,
|
||||
headers: { Authorization: 'Bearer e2e-test-token' },
|
||||
toolCallTimeoutMs: 15_000,
|
||||
failOnStartupError: false,
|
||||
}
|
||||
await apply(ctx, config)
|
||||
}, 30_000)
|
||||
|
||||
@@ -713,6 +713,7 @@ describe('createTransport', () => {
|
||||
env: {},
|
||||
cwd: '/tmp',
|
||||
toolCallTimeoutMs: 60_000,
|
||||
failOnStartupError: false,
|
||||
}
|
||||
const transport = createTransport(config)
|
||||
expect(transport).toBeDefined()
|
||||
@@ -727,6 +728,7 @@ describe('createTransport', () => {
|
||||
url: 'http://localhost:3000/mcp',
|
||||
headers: {},
|
||||
toolCallTimeoutMs: 60_000,
|
||||
failOnStartupError: false,
|
||||
}
|
||||
const transport = createTransport(config)
|
||||
expect(transport).toBeDefined()
|
||||
@@ -741,6 +743,7 @@ describe('createTransport', () => {
|
||||
url: 'http://localhost:3000/mcp',
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
toolCallTimeoutMs: 60_000,
|
||||
failOnStartupError: false,
|
||||
}
|
||||
const transport = createTransport(config)
|
||||
expect(transport).toBeDefined()
|
||||
@@ -764,6 +767,7 @@ describe('createTransport', () => {
|
||||
env: { EXTRA: 'injected' },
|
||||
cwd: '',
|
||||
toolCallTimeoutMs: 60_000,
|
||||
failOnStartupError: false,
|
||||
}
|
||||
// createTransport internally calls buildChildEnv; we verify by inspecting
|
||||
// the constructed StdioClientTransport. Since we can't inspect private fields
|
||||
@@ -791,6 +795,7 @@ describe('createTransport', () => {
|
||||
env: { CUSTOM: 'value' },
|
||||
cwd: '',
|
||||
toolCallTimeoutMs: 60_000,
|
||||
failOnStartupError: false,
|
||||
}
|
||||
const transport = createTransport(config)
|
||||
expect(transport).toBeDefined()
|
||||
|
||||
Reference in New Issue
Block a user