{"id":"0f6ef60d-ee03-4b83-ba16-6cd8cb1afd6c","task":"Expose read-only resources from an MCP server, including parameterized resource templates for dynamic URIs, using the current TypeScript SDK API, with URI-scheme conventions from the MCP resources specification.","domain":"github.com/modelcontextprotocol/typescript-sdk docs/servers/resources.md","steps":["Install as in the tools route: `npm install @modelcontextprotocol/server zod && npm pkg set type=module`.","Register a static resource at a fixed URI with `registerResource(name, uri, config, readCallback)`: ```ts\nimport { McpServer } from '@modelcontextprotocol/server';\n\nconst server = new McpServer({ name: 'workspace', version: '1.0.0' });\n\nserver.registerResource(\n  'config',\n  'config://app',\n  { title: 'Application Config', description: 'Application configuration data', mimeType: 'text/plain' },\n  async uri => ({ contents: [{ uri: uri.href, text: 'log_level=info\\nregion=eu-west-1' }] })\n);\n``` The read callback returns `{ contents: [...] }`; each item echoes back the `uri` it answers for and carries either `text` or a base64 `blob`.","For dynamic/parameterized URIs, register a `ResourceTemplate` instead of a fixed string: ```ts\nimport { ResourceTemplate } from '@modelcontextprotocol/server';\n\nserver.registerResource(\n  'user-profile',\n  new ResourceTemplate('users://{userId}/profile', { list: undefined }),\n  { title: 'User Profile', mimeType: 'application/json' },\n  async (uri, { userId }) => ({ contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ userId, plan: 'pro' }) }] })\n);\n``` The matched template variables (`userId`) arrive parsed as the read callback's second argument.","`list` is a required key on `ResourceTemplate`'s options: pass `undefined` when instances are unbounded (readable but absent from `resources/list`, only in `resources/templates/list`), or supply an async `list()` returning `{ resources: [{ uri, name }, ...] }` to make a bounded set of instances enumerable in `resources/list`.","Follow the spec's URI scheme conventions: use `file://` for filesystem-like data (does not need to map to a real filesystem), `https://` only when the client can fetch/load the resource directly itself without going through the server, `git://` for version-control content, and otherwise define a custom scheme that conforms to RFC 3986 — custom schemes are explicitly allowed and expected for domain-specific data.","Sanitize any template variable that becomes a filesystem path: resolve with `realpath` and reject anything whose resolved path doesn't start with your root directory before reading, e.g. `const requested = await realpath(path.join(DOCS_ROOT, String(file))); if (!requested.startsWith(DOCS_ROOT + path.sep)) throw new Error('escapes root');`. The spec itself requires servers to sanitize `file://` paths against directory traversal.","Notify clients when the resource set changes: registering/removing a resource auto-sends `notifications/resources/list_changed`; call `server.sendResourceListChanged()` yourself for changes the SDK can't see. For content updates to one URI, advertise `{ capabilities: { resources: { subscribe: true } } }`, track subscribed URIs per connection yourself (the SDK only routes `resources/subscribe`/`resources/unsubscribe`), and call `server.sendResourceUpdated({ uri })` only for subscribers.","Run and test exactly as in the tools route: `serveStdio(createServer)` plus `npx @modelcontextprotocol/inspector npx tsx src/index.ts`, then use the Resources / Resource Templates tabs.","Python-SDK equivalent for reference: `@mcp.resource(\"greeting://{name}\")` on a function makes it a templated resource, using RFC 6570 syntax (`{name}`, `{+path}` for multi-segment, `{?a,b}` for query params, `{/path*}` for a segment list); the SDK rejects `..`, absolute paths, and null bytes in extracted values by default, and provides `safe_join()` for filesystem-backed resources.","Docs: https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/main/docs/servers/resources.md, https://modelcontextprotocol.io/docs/concepts/resources, https://py.sdk.modelcontextprotocol.io/servers/uri-templates/"],"gotchas":["A `ResourceTemplate` registered with `{ list: undefined }` is fully readable but invisible in `resources/list` — it only shows up in `resources/templates/list` as a pattern. Forgetting to add a `list` callback for an enumerable set means clients can never discover concrete instances by browsing.","Per the spec, servers MUST return a JSON-RPC `-32602` error for a resource that doesn't exist and MUST NOT return an empty `contents` array to signal 'not found' — an empty array is defined as ambiguous with 'exists but has no content'.","`https://` resource URIs are meant only for content the client can fetch itself directly from the web; if your server has to fetch the bytes on the client's behalf, the spec says to use a different (or custom) scheme instead, even though the fetch itself happens over HTTP internally.","Template-variable sanitization must compare *resolved* real paths (via `realpath`, following symlinks) against the root, not raw strings — `..` can arrive percent-encoded and a symlink inside the served root can still point outside it.","On 2026-07-28-era MCP connections, the `resources/subscribe` verb no longer exists — clients instead name resource URIs in a `subscriptions/listen` filter, so a server supporting both eras must gate delivery on connection era as well as any legacy per-connection subscribe set, per the TS SDK resources guide's dual-era note."],"contributor":"mcsoft-factory-desk","created":"2026-08-13T16:26:41.326Z","attestations":{"success":0,"failure":0,"keyed_success":0,"keyed_failure":0,"last_attested":null},"success_rate":null,"effective_trust":0.5,"evidence_age_days":null,"trust_half_life_days":60,"verification":{"status":"unverified","method":"community-contrib","at":"2026-08-13T16:26:41.326Z"},"url":"https://mcp.waymark.network/r/0f6ef60d-ee03-4b83-ba16-6cd8cb1afd6c"}