diff --git a/src/server/completable.ts b/src/server/completable.ts index 67d91c383..04f4b308d 100644 --- a/src/server/completable.ts +++ b/src/server/completable.ts @@ -47,6 +47,13 @@ export class Completable extends ZodType(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; +} + /** * Wraps a Zod type to provide autocompletion capabilities. Useful for, e.g., prompt arguments in MCP. */ diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 4bb42d7fc..f8e87177c 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -2285,6 +2285,41 @@ describe('resource()', () => { ).rejects.toThrow(/Resource test:\/\/nonexistent not found/); }); + /*** + * 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 */ @@ -3137,6 +3172,46 @@ describe('prompt()', () => { ).rejects.toThrow(/Prompt nonexistent-prompt not found/); }); + /*** + * 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 */ @@ -3172,7 +3247,7 @@ describe('prompt()', () => { await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); - expect(client.getServerCapabilities()).toMatchObject({ completions: {} }); + expect(client.getServerCapabilities()).toMatchObject({ completions: {}, prompts: { listChanged: true } }); }); /*** diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 765ba864f..b9ec67c78 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -33,7 +33,7 @@ import { ToolAnnotations, LoggingMessageNotification } from '../types.js'; -import { Completable, CompletableDef } 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'; @@ -245,7 +245,7 @@ export class McpServer { } const field = prompt.argsSchema.shape[request.params.argument.name]; - if (!(field instanceof Completable)) { + if (!isCompletable(field)) { return EMPTY_COMPLETION_RESULT; } @@ -353,8 +353,6 @@ export class McpServer { throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} not found`); }); - this.setCompletionRequestHandler(); - this._resourceHandlersInitialized = true; } @@ -418,8 +416,6 @@ export class McpServer { } }); - this.setCompletionRequestHandler(); - this._promptHandlersInitialized = true; } @@ -606,6 +602,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; } @@ -639,6 +643,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; }