From a57d59d268a42a3a32c60a7c43c5776f30412c2e Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 4 Dec 2025 16:09:43 -0500 Subject: [PATCH] * 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 `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/mcp.test.ts | 75 ++++++++++++++++++++++++++++++++++++++++++ src/server/mcp.ts | 25 +++++++++++--- 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 7dc4742e6..8dc98ff6d 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -2669,6 +2669,41 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { ).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 */ @@ -3548,6 +3583,46 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { ).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 */ diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 1617dc37b..1ecdc43d6 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -62,6 +62,7 @@ import { Transport } from '../shared/transport.js'; import { validateAndWarnToolName } from '../shared/toolNameValidation.js'; import { ExperimentalMcpServerTasks } from '../experimental/tasks/mcp-server.js'; import type { ToolTaskHandler } from '../experimental/tasks/interfaces.js'; +import { ZodOptional } from 'zod'; /** * High-level MCP server that provides a simpler API for working with resources, tools, and prompts. @@ -557,8 +558,6 @@ export class McpServer { throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} not found`); }); - this.setCompletionRequestHandler(); - this._resourceHandlersInitialized = true; } @@ -623,8 +622,6 @@ export class McpServer { } }); - this.setCompletionRequestHandler(); - this._promptHandlersInitialized = true; } @@ -815,6 +812,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 +853,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; }