From 5271234d7daa0ca2e012a19ca47db2b6080b9e7a Mon Sep 17 00:00:00 2001 From: oratis Date: Sun, 31 May 2026 23:41:28 +0800 Subject: [PATCH] feat(mcp): list resource templates on connect MCP servers can expose parameterized resource templates (resources/templates/ list, e.g. `file:///{path}`). connectMcpServer now lists them (best-effort, within the `resources` capability) onto McpClientHandle.resourceTemplates, so the host/model can see what parameterized resources a server offers. +1 test (spawned server advertising a template). Core 618 green. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/index.ts | 1 + packages/core/src/mcp/client.test.ts | 9 ++++++++- packages/core/src/mcp/client.ts | 24 ++++++++++++++++++++++++ packages/core/src/mcp/index.ts | 1 + 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 084cc30..f5078f9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -214,6 +214,7 @@ export { type McpClientHandle, type McpToolMeta, type McpResourceMeta, + type McpResourceTemplateMeta, type McpPromptMeta, type ConnectAllResult, type BuildMcpServerOpts, diff --git a/packages/core/src/mcp/client.test.ts b/packages/core/src/mcp/client.test.ts index 3ae92b6..b47b078 100644 --- a/packages/core/src/mcp/client.test.ts +++ b/packages/core/src/mcp/client.test.ts @@ -70,7 +70,7 @@ async function writeFakeServer( : ''; const resourceBlock = resources ? ` -import { ListResourcesRequestSchema, ReadResourceRequestSchema } from '${TYPES_INDEX}'; +import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema } from '${TYPES_INDEX}'; const RESOURCES = ${JSON.stringify(resources)}; server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: RESOURCES.map((r) => ({ uri: r.uri, name: r.name, mimeType: r.mimeType })), @@ -80,6 +80,9 @@ server.setRequestHandler(ReadResourceRequestSchema, async (req) => { if (!found) throw new Error('no such resource: ' + req.params.uri); return { contents: [{ uri: found.uri, mimeType: found.mimeType ?? 'text/plain', text: found.text }] }; }); +server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ + resourceTemplates: [{ uriTemplate: 'file:///{path}', name: 'file', description: 'a file by path' }], +})); ` : ''; const promptBlock = prompts @@ -279,6 +282,10 @@ describe('MCP client', () => { 'mem://note', ]); + // resources/templates/list populated the handle's templates + expect(handle.resourceTemplates.map((t) => t.uriTemplate)).toEqual(['file:///{path}']); + expect(handle.resourceTemplates[0]!.name).toBe('file'); + // readMcpResource flattens contents to text expect(await readMcpResource(handle, 'file:///readme.md')).toContain('# Hello'); diff --git a/packages/core/src/mcp/client.ts b/packages/core/src/mcp/client.ts index 6a533dc..21ed2d3 100644 --- a/packages/core/src/mcp/client.ts +++ b/packages/core/src/mcp/client.ts @@ -39,6 +39,15 @@ export interface McpResourceMeta { mimeType?: string; } +/** A parameterized resource a server exposes (from resources/templates/list). */ +export interface McpResourceTemplateMeta { + /** RFC 6570 URI template, e.g. `file:///{path}`. */ + uriTemplate: string; + name: string; + description?: string; + mimeType?: string; +} + /** A prompt a server exposes (from prompts/list). */ export interface McpPromptMeta { name: string; @@ -88,6 +97,8 @@ export interface McpClientHandle { tools: ToolHandler[]; /** Resources the server advertised (empty if it has no `resources` capability). */ resources: McpResourceMeta[]; + /** Parameterized resource templates the server advertised. */ + resourceTemplates: McpResourceTemplateMeta[]; /** Prompts the server advertised (empty if it has no `prompts` capability). */ prompts: McpPromptMeta[]; close(): Promise; @@ -295,6 +306,7 @@ export async function connectMcpServer( // Resources (best-effort, capability-gated). A server without the `resources` // capability — or one that errors on resources/list — just yields []. let resources: McpResourceMeta[] = []; + let resourceTemplates: McpResourceTemplateMeta[] = []; if (client.getServerCapabilities()?.resources) { try { const r = await client.listResources(); @@ -307,6 +319,17 @@ export async function connectMcpServer( } catch { /* server advertised resources but list failed — degrade to none */ } + try { + const rt = await client.listResourceTemplates(); + resourceTemplates = (rt.resourceTemplates ?? []).map((t) => ({ + uriTemplate: t.uriTemplate, + name: t.name, + description: t.description, + mimeType: t.mimeType, + })); + } catch { + /* templates are optional even within the resources capability */ + } } // Prompts (best-effort, capability-gated — same degradation as resources). @@ -331,6 +354,7 @@ export async function connectMcpServer( transportKind: kind, tools, resources, + resourceTemplates, prompts, async close() { await client.close(); diff --git a/packages/core/src/mcp/index.ts b/packages/core/src/mcp/index.ts index d702cba..70b28f1 100644 --- a/packages/core/src/mcp/index.ts +++ b/packages/core/src/mcp/index.ts @@ -18,6 +18,7 @@ export { type McpClientHandle, type McpToolMeta, type McpResourceMeta, + type McpResourceTemplateMeta, type McpPromptMeta, type McpTransportKind, type ConnectAllResult,