Skip to content
Open
5 changes: 5 additions & 0 deletions .changeset/honor-disable-on-resource-templates.md
Original file line number Diff line number Diff line change
@@ -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.
20 changes: 15 additions & 5 deletions packages/server/src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 };
});
Expand Down Expand Up @@ -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);
}
}
Expand Down
198 changes: 198 additions & 0 deletions test/integration/test/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
*/
Expand Down
Loading