From 8c1aefc771adba0d03033d8972be2d0effd70536 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 11 Oct 2025 18:27:58 -0400 Subject: [PATCH 1/2] * In completable.ts - add `isCompletable` function, a runtime type guard to detect `Completable`-wrapped Zod types * In mcp.test.ts - Added tests - "should not advertise support for completion when a resource template without a complete callback is defined" - "should not advertise support for completion when a prompt without a completable argument is defined" * In mcp.ts - in `handlePromptCompletion` method, - fix warning "Invalid 'instanceof' check: 'field' has type that is not related to 'Completable'" by using the new `isCompletable` function from `completable.ts` - in `setResourceRequestHandlers` method - remove unconditional call to `setCompletionRequestHandler` - supporting resources does not automatically mean that resource template completion is supported - in `setPromptRequestHandlers` method - remove unconditional call to `setCompletionRequestHandler` - supporting prompts does not automatically mean that prompt argument completion is supported - in `_createRegisteredResourceTemplate` method - check if the resource template being registered has a complete callback, - if so, call `setCompletionRequestHandler` - in _`createRegisteredPrompt` method - check if any argument of the prompt has a `Completable` schema - if so, call `setCompletionRequestHandler` --- src/server/completable.ts | 7 ++++ src/server/mcp.test.ts | 85 ++++++++++++++++++++++++++++++++++++++- src/server/mcp.ts | 28 ++++++++++--- 3 files changed, 113 insertions(+), 7 deletions(-) diff --git a/src/server/completable.ts b/src/server/completable.ts index 67d91c383..c3c489f5c 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..097ee9ac3 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -2285,6 +2285,45 @@ 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 +3176,50 @@ 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 +3255,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 cef1722d6..d93c714ad 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'; @@ -243,7 +243,7 @@ export class McpServer { } const field = prompt.argsSchema.shape[request.params.argument.name]; - if (!(field instanceof Completable)) { + if (!isCompletable(field)) { return EMPTY_COMPLETION_RESULT; } @@ -351,8 +351,6 @@ export class McpServer { throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} not found`); }); - this.setCompletionRequestHandler(); - this._resourceHandlersInitialized = true; } @@ -416,8 +414,6 @@ export class McpServer { } }); - this.setCompletionRequestHandler(); - this._promptHandlersInitialized = true; } @@ -604,6 +600,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; } @@ -637,6 +641,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; } From 9c94f26cd1bfbc9f88d3719e6011ee84da5c621d Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 11 Oct 2025 18:39:33 -0400 Subject: [PATCH 2/2] Run prettier --- src/server/completable.ts | 2 +- src/server/mcp.test.ts | 78 ++++++++++++++++++--------------------- src/server/mcp.ts | 4 +- 3 files changed, 38 insertions(+), 46 deletions(-) diff --git a/src/server/completable.ts b/src/server/completable.ts index c3c489f5c..04f4b308d 100644 --- a/src/server/completable.ts +++ b/src/server/completable.ts @@ -49,7 +49,7 @@ export class Completable extends ZodType(value: unknown): value is Completable { - if (value === null || typeof value !== "object") return false; + if (value === null || typeof value !== 'object') return false; const obj = value as { _def?: { typeName?: unknown } }; return obj._def?.typeName === McpZodTypeKind.Completable; } diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 097ee9ac3..f8e87177c 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -2288,41 +2288,37 @@ describe('resource()', () => { /*** * 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 () => { + 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", + name: 'test server', + version: '1.0' }); const client = new Client({ - name: "test client", - version: "1.0", + name: 'test client', + version: '1.0' }); mcpServer.resource( - "test", - new ResourceTemplate("test://resource/{category}", { - list: undefined, + 'test', + new ResourceTemplate('test://resource/{category}', { + list: undefined }), async () => ({ contents: [ { - uri: "test://resource/test", - text: "Test content", - }, - ], - }), + uri: 'test://resource/test', + text: 'Test content' + } + ] + }) ); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - await Promise.all([ - client.connect(clientTransport), - mcpServer.server.connect(serverTransport), - ]); + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); - expect(client.getServerCapabilities()).not.toHaveProperty("completions") - }) + expect(client.getServerCapabilities()).not.toHaveProperty('completions'); + }); /*** * Test: Registering a resource template with a complete callback should update server capabilities to advertise support for completion @@ -3179,46 +3175,42 @@ describe('prompt()', () => { /*** * 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 () => { + 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", + name: 'test server', + version: '1.0' }); const client = new Client({ - name: "test client", - version: "1.0", + name: 'test client', + version: '1.0' }); mcpServer.prompt( - "test-prompt", + 'test-prompt', { name: z.string() }, async ({ name }) => ({ messages: [ { - role: "assistant", + role: 'assistant', content: { - type: "text", - text: `Hello ${name}`, - }, - }, - ], - }), + type: 'text', + text: `Hello ${name}` + } + } + ] + }) ); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); - await Promise.all([ - client.connect(clientTransport), - mcpServer.server.connect(serverTransport), - ]); + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); - const capabilities = client.getServerCapabilities()||{}; + 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 @@ -3255,7 +3247,7 @@ describe('prompt()', () => { await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); - expect(client.getServerCapabilities()).toMatchObject({ completions: {}, "prompts": {"listChanged": true} }); + expect(client.getServerCapabilities()).toMatchObject({ completions: {}, prompts: { listChanged: true } }); }); /*** diff --git a/src/server/mcp.ts b/src/server/mcp.ts index d93c714ad..5ae1d1c8a 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -603,7 +603,7 @@ export class McpServer { // 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)); + const hasCompleter = Array.isArray(variableNames) && variableNames.some(v => !!template.completeCallback(v)); if (hasCompleter) { this.setCompletionRequestHandler(); } @@ -644,7 +644,7 @@ export class McpServer { // If any argument uses a Completable schema, enable completions capability if (argsSchema) { - const hasCompletable = Object.values(argsSchema).some((field) => { + const hasCompletable = Object.values(argsSchema).some(field => { const inner: unknown = field instanceof ZodOptional ? field._def?.innerType : field; return isCompletable(inner); });