Skip to content

[v2] disable() has no effect on resource templates #2696

Description

@Grit03

What happened?

A resource registered with a ResourceTemplate stays listed and stays readable after disable(). The enabled flag flips to false correctly, but it is not properly applied in the four request methods below:

  • resources/list — the template loop calls listCallback and merges its results without checking enabled
  • resources/templates/list — no filter at all, every registered template is returned
  • resources/read — the template branch invokes readCallback as soon as the URI matches
  • completion/complete with a ref/resourcehandleResourceCompletion runs the completer without checking enabled

What did you expect?

The same behavior as a disabled static URI resource in resource requests:

  • the listing methods leave it out of the response
  • resources/read throws a ProtocolError (InvalidParams)

The same behavior as a disabled prompt in a completion request:

  • completion/complete throws a ProtocolError

Code to reproduce

import { Client, InMemoryTransport } from '@modelcontextprotocol/client';
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/server';

const server = new McpServer({ name: 'repro', version: '0.0.0' });

const tool = server.registerTool('echo', { description: 'echo' }, async () => ({
    content: [{ type: 'text' as const, text: 'echo' }]
}));

const fixed = server.registerResource('static', 'demo://static', {}, async uri => ({
    contents: [{ uri: uri.href, text: 'static content' }]
}));

const template = server.registerResource(
    'user',
    new ResourceTemplate('demo://users/{id}', {
        list: async () => ({ resources: [{ uri: 'demo://users/1', name: 'user 1' }] })
    }),
    {},
    async (uri, vars) => ({ contents: [{ uri: uri.href, text: `profile of ${vars.id}` }] })
);

const [ct, st] = InMemoryTransport.createLinkedPair();
const client = new Client({ name: 'repro-client', version: '0.0.0' });
await Promise.all([client.connect(ct), server.server.connect(st)]);

tool.disable();
fixed.disable();
template.disable();

console.log('tools/list         :', (await client.listTools()).tools.map(t => t.name));
// [] — the tool honors disable()

console.log('read demo://static :', await client.readResource({ uri: 'demo://static' }).then(r => r.contents[0]).catch(e => 'threw: ' + e.message));
// threw: Resource demo://static disabled — the static resource honors disable()

console.log('resources/list     :', (await client.listResources()).resources.map(r => r.uri));
// expected [], got [ 'demo://users/1' ]

console.log('templates/list     :', (await client.listResourceTemplates()).resourceTemplates.map(t => t.uriTemplate));
// expected [], got [ 'demo://users/{id}' ]

console.log('read demo://users/1:', await client.readResource({ uri: 'demo://users/1' }).then(r => r.contents[0]).catch(e => 'threw: ' + e.message));
// expected a throw, got { uri: 'demo://users/1', text: 'profile of 1' }

Result

tools/list         : []
read demo://static : threw: Resource demo://static disabled
resources/list     : [ 'demo://users/1' ]   # expected []
templates/list     : [ 'demo://users/{id}' ]  # expected []
read demo://users/1: { uri: 'demo://users/1', text: 'profile of 1' }   # expected a throw

SDK version

@modelcontextprotocol/server 2.0.0, @modelcontextprotocol/client 2.0.0 (main @ 3924de9)

Area

Server

Metadata

Metadata

Assignees

No one assigned

    Labels

    v1Issues / PRs related to v1.xv2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions