diff --git a/src/server/completable.ts b/src/server/completable.ts index 652eaf72e..08504741c 100644 --- a/src/server/completable.ts +++ b/src/server/completable.ts @@ -61,6 +61,13 @@ export class Completable extends ZodType< }; } +// 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; +} + /** * 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 d9142702f..009981e19 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -2479,6 +2479,47 @@ 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.toMatchObject({ completions: {} }) + }) + + /*** * Test: Registering a resource template with a complete callback should update server capabilities to advertise support for completion */ @@ -3440,6 +3481,49 @@ describe("prompt()", () => { expect(client.getServerCapabilities()).toMatchObject({ completions: {} }) }) + + /*** + * 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), + ]); + + expect(client.getServerCapabilities()).not.toMatchObject({ completions: {} }) + }) + /*** * Test: Prompt Argument Completion */ diff --git a/src/server/mcp.ts b/src/server/mcp.ts index ac4880c99..9f3fa0795 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -43,7 +43,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"; @@ -290,7 +290,7 @@ export class McpServer { } const field = prompt.argsSchema.shape[request.params.argument.name]; - if (!(field instanceof Completable)) { + if (!isCompletable(field)) { return EMPTY_COMPLETION_RESULT; } @@ -439,8 +439,6 @@ export class McpServer { }, ); - this.setCompletionRequestHandler(); - this._resourceHandlersInitialized = true; } @@ -523,8 +521,6 @@ export class McpServer { }, ); - this.setCompletionRequestHandler(); - this._promptHandlersInitialized = true; } @@ -731,6 +727,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; } @@ -764,6 +768,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; }