diff --git a/.changeset/honor-disable-on-resource-templates.md b/.changeset/honor-disable-on-resource-templates.md new file mode 100644 index 0000000000..7d273906ab --- /dev/null +++ b/.changeset/honor-disable-on-resource-templates.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/server': patch +--- + +Honor `disable()` on resource templates. `resources/list`, `resources/templates/list`, `resources/read` and `completion/complete` never read the `enabled` flag, so a disabled template stayed listed, readable, and completable. diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index d2e40181e4..b7cc14905f 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -411,6 +411,10 @@ export class McpServer { throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource template ${request.params.ref.uri} not found`); } + if (!template.enabled) { + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource template ${request.params.ref.uri} disabled`); + } + const completer = template.resourceTemplate.completeCallback(request.params.argument.name); if (!completer) { return EMPTY_COMPLETION_RESULT; @@ -448,6 +452,7 @@ export class McpServer { const templateResources: Resource[] = []; for (const template of Object.values(this._registeredResourceTemplates)) { + if (!template.enabled) continue; if (!template.resourceTemplate.listCallback) { continue; } @@ -466,11 +471,13 @@ export class McpServer { }); this.server.setRequestHandler('resources/templates/list', async () => { - const resourceTemplates = Object.entries(this._registeredResourceTemplates).map(([name, template]) => ({ - name, - uriTemplate: template.resourceTemplate.uriTemplate.toString(), - ...template.metadata - })); + const resourceTemplates = Object.entries(this._registeredResourceTemplates) + .filter(([_, t]) => t.enabled) + .map(([name, template]) => ({ + name, + uriTemplate: template.resourceTemplate.uriTemplate.toString(), + ...template.metadata + })); return { resourceTemplates }; }); @@ -502,6 +509,9 @@ export class McpServer { for (const template of Object.values(this._registeredResourceTemplates)) { const variables = template.resourceTemplate.uriTemplate.match(uri.toString()); if (variables) { + if (!template.enabled) { + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource template ${uri} disabled`); + } return attachCacheHintFallback(await template.readCallback(uri, variables, ctx), template.cacheHint); } } diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index 4b9a3865f0..86dbc1e62b 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -2385,6 +2385,52 @@ describe('Zod v4', () => { expect(result2.resourceTemplates).toHaveLength(0); }); + /*** + * Test: Disabled Resource Templates Are Omitted from resources/templates/list + */ + test('should omit disabled resource templates from resources/templates/list', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + // Register resource template + const resourceTemplate = mcpServer.registerResource( + 'template', + new ResourceTemplate('test://resource/{id}', { list: undefined }), + {}, + async uri => ({ + contents: [ + { + uri: uri.href, + text: 'Template content' + } + ] + }) + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); + + // Verify template is registered + const result = await client.request({ method: 'resources/templates/list' }); + + expect(result.resourceTemplates).toHaveLength(1); + + // Disable the template + resourceTemplate.disable(); + + // Verify the template was disabled + const result2 = await client.request({ method: 'resources/templates/list' }); + + expect(result2.resourceTemplates).toHaveLength(0); + }); + /*** * Test: Resource Registration with Metadata */ @@ -2529,6 +2575,52 @@ describe('Zod v4', () => { expect(result.resources[1]!.uri).toBe('test://resource/2'); }); + /*** + * Test: Disabled Resources and Resource Templates Are Omitted from resources/list + */ + test('should omit disabled resources and resource template listings from resources/list', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + const disabledResource = mcpServer.registerResource('inactive', 'test://static/inactive', {}, async uri => ({ + contents: [{ uri: uri.href, text: 'Inactive content' }] + })); + + mcpServer.registerResource( + 'active-template', + new ResourceTemplate('test://active/{id}', { + list: async () => ({ resources: [{ name: 'Active 1', uri: 'test://active/1' }] }) + }), + {}, + async uri => ({ contents: [{ uri: uri.href, text: 'Active template content' }] }) + ); + const disabledTemplate = mcpServer.registerResource( + 'inactive-template', + new ResourceTemplate('test://inactive/{id}', { + list: async () => ({ resources: [{ name: 'Inactive 1', uri: 'test://inactive/1' }] }) + }), + {}, + async uri => ({ contents: [{ uri: uri.href, text: 'Inactive template content' }] }) + ); + + disabledResource.disable(); + disabledTemplate.disable(); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + const result = await client.request({ method: 'resources/list' }); + + expect(result.resources.map(resource => resource.uri)).toEqual(['test://active/1']); + }); + /*** * Test: Template Variables to Read Callback */ @@ -2858,6 +2950,112 @@ describe('Zod v4', () => { }); }); + /*** + * Test: ProtocolError for Disabled Resource Template + */ + test('should throw ProtocolError for disabled resource template', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + const resourceTemplate = mcpServer.registerResource( + 'test', + new ResourceTemplate('test://resource/{id}', { list: undefined }), + {}, + async uri => ({ + contents: [ + { + uri: uri.href, + text: 'Template content' + } + ] + }) + ); + + resourceTemplate.disable(); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + await expect( + client.request({ + method: 'resources/read', + params: { + uri: 'test://resource/1' + } + }) + ).rejects.toMatchObject({ + code: ProtocolErrorCode.InvalidParams, + message: expect.stringContaining('disabled') + }); + }); + + /*** + * Test: ProtocolError for Completion on a Disabled Resource Template + */ + test('should throw ProtocolError for completion of a disabled resource template', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + const resourceTemplate = mcpServer.registerResource( + 'test', + new ResourceTemplate('test://resource/{category}', { + list: undefined, + complete: { + category: () => ['books', 'movies', 'music'] + } + }), + {}, + async () => ({ + contents: [ + { + uri: 'test://resource/test', + text: 'Test content' + } + ] + }) + ); + + resourceTemplate.disable(); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); + + await expect( + client.request({ + method: 'completion/complete', + params: { + ref: { + type: 'ref/resource', + uri: 'test://resource/{category}' + }, + argument: { + name: 'category', + value: '' + } + } + }) + ).rejects.toMatchObject({ + code: ProtocolErrorCode.InvalidParams, + message: expect.stringContaining('disabled') + }); + }); + /*** * Test: Registering a resource template without a complete callback should not update server capabilities to advertise support for completion */