diff --git a/src/server/completable.ts b/src/server/completable.ts index be067ac55..d6548181b 100644 --- a/src/server/completable.ts +++ b/src/server/completable.ts @@ -46,6 +46,13 @@ export function getCompleter(schema: T): CompleteCallback | undefined; } +// Runtime type guard to detect Completable-wrapped Zod types across versions +export function isCompletable(value: unknown): value is Completable { + if (value === null || typeof value !== 'object') return false; + const obj = value as { _def?: { typeName?: unknown } }; + return obj._def?.typeName === McpZodTypeKind.Completable; +} + /** * Unwraps a completable schema to get the underlying schema. * For backward compatibility with code that called `.unwrap()`. diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 7dc4742e6..91493c4ae 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -4752,9 +4752,53 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { }; }); - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - await server.connect(serverTransport); - await client.connect(clientTransport); + /*** + * Test: Registering a resource template without a complete callback should not update server capabilities to advertise support for completion + */ + test('should not advertise support for completion when a resource template without a complete callback is defined', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + mcpServer.resource( + 'test', + new ResourceTemplate('test://resource/{category}', { + list: undefined + }), + async () => ({ + contents: [ + { + uri: 'test://resource/test', + text: 'Test content' + } + ] + }) + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + expect(client.getServerCapabilities()).not.toHaveProperty('completions'); + }); + + /*** + * Test: Registering a resource template with a complete callback should update server capabilities to advertise support for completion + */ + test('should advertise support for completion when a resource template with a complete callback is defined', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + const client = new Client({ + name: 'test client', + version: '1.0' + }); const invalidTypeResult = await client.callTool({ name: 'union-test', @@ -5800,14 +5844,57 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { } }); - expect(checkAvailability).toHaveBeenCalledWith('ABC Restaurant', '2024-12-25', 2); - expect(findAlternatives).not.toHaveBeenCalled(); - expect(result.content).toEqual([ - { - type: 'text', - text: 'No booking made. Original date not available.' - } - ]); + /*** + * Test: Registering a prompt without a completable argument should not update server capabilities to advertise support for completion + */ + test('should not advertise support for completion when a prompt without a completable argument is defined', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + mcpServer.prompt( + 'test-prompt', + { + name: z.string() + }, + async ({ name }) => ({ + messages: [ + { + role: 'assistant', + content: { + type: 'text', + text: `Hello ${name}` + } + } + ] + }) + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + const capabilities = client.getServerCapabilities() || {}; + const keys = Object.keys(capabilities); + expect(keys).not.toContain('completions'); + }); + + /*** + * Test: Registering a prompt with a completable argument should update server capabilities to advertise support for completion + */ + test('should advertise support for completion when a prompt with a completable argument is defined', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + const client = new Client({ + name: 'test client', + version: '1.0' }); test('should handle user cancelling the elicitation', async () => { @@ -5823,7 +5910,8 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + expect(client.getServerCapabilities()).toMatchObject({ completions: {}, prompts: { listChanged: true } }); + }); // Call the tool const result = await client.callTool({ diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 1617dc37b..11d10a910 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -54,7 +54,7 @@ import { CallToolRequest, ToolExecution } from '../types.js'; -import { isCompletable, getCompleter } from './completable.js'; +import { CompletableDef, isCompletable } from './completable.js'; import { UriTemplate, Variables } from '../shared/uriTemplate.js'; import { RequestHandlerExtra } from '../shared/protocol.js'; import { Transport } from '../shared/transport.js'; @@ -441,9 +441,8 @@ export class McpServer { return EMPTY_COMPLETION_RESULT; } - const promptShape = getObjectShape(prompt.argsSchema); - const field = promptShape?.[request.params.argument.name]; - if (!isCompletable(field)) { + const field = prompt.argsSchema.shape[request.params.argument.name]; + if (!isCompletable(field)) { return EMPTY_COMPLETION_RESULT; } @@ -557,8 +556,6 @@ export class McpServer { throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} not found`); }); - this.setCompletionRequestHandler(); - this._resourceHandlersInitialized = true; } @@ -623,8 +620,6 @@ export class McpServer { } }); - this.setCompletionRequestHandler(); - this._promptHandlersInitialized = true; } @@ -815,6 +810,14 @@ export class McpServer { } }; this._registeredResourceTemplates[name] = registeredResourceTemplate; + + // If the resource template has any completion callbacks, enable completions capability + const variableNames = template.uriTemplate.variableNames; + const hasCompleter = Array.isArray(variableNames) && variableNames.some(v => !!template.completeCallback(v)); + if (hasCompleter) { + this.setCompletionRequestHandler(); + } + return registeredResourceTemplate; } @@ -848,6 +851,18 @@ export class McpServer { } }; this._registeredPrompts[name] = registeredPrompt; + + // If any argument uses a Completable schema, enable completions capability + if (argsSchema) { + const hasCompletable = Object.values(argsSchema).some(field => { + const inner: unknown = field instanceof ZodOptional ? field._def?.innerType : field; + return isCompletable(inner); + }); + if (hasCompletable) { + this.setCompletionRequestHandler(); + } + } + return registeredPrompt; }