Resources and prompts in TypeScript
- Spec
2026-07-28- Evidence
- E2Reproducedarm64
- TypeScript SDK (server)
@modelcontextprotocol/server@2.4.0- MCP Inspector
@modelcontextprotocol/inspector@2.12.0- Verified
Tools are for actions the model decides to take. A server can offer two more things:
- Resources: data the host application reads, each with a URI. A file, a database row, a note.
- Prompts: message templates the user picks, usually from a menu or a slash command.
This guide builds a small notes server with all three. It uses stdio like the first server; everything here works the same over HTTP.
The server
Section titled “The server”import { McpServer, ResourceNotFoundError, ResourceTemplate } from '@modelcontextprotocol/server';import { serveStdio } from '@modelcontextprotocol/server/stdio';import * as z from 'zod';
// In a real server this would be a database or a folder of files.const notes: Record<string, { title: string; body: string }> = { welcome: { title: 'Welcome', body: 'MCP servers offer tools, resources and prompts.' }, release: { title: 'Release checklist', body: 'Run the tests, bump the version, tag the commit.' },};
serveStdio(() => { const server = new McpServer({ name: 'notes-ts', version: '1.0.0' });
// A fixed resource: one URI, one piece of content. server.registerResource( 'note-index', 'notes://index', { title: 'All notes', description: 'One line per note.', mimeType: 'text/plain' }, async (uri) => ({ contents: [{ uri: uri.href, text: Object.entries(notes).map(([id, n]) => `${id}: ${n.title}`).join('\n') }], }), );
// A resource template: one pattern, many URIs. `list` tells clients which ones exist. server.registerResource( 'note', new ResourceTemplate('notes://{id}', { list: async () => ({ resources: Object.entries(notes).map(([id, n]) => ({ uri: `notes://${id}`, name: id, title: n.title })), }), }), { title: 'Note', mimeType: 'text/markdown' }, async (uri, { id }) => { const note = notes[String(id)]; // Sent as JSON-RPC error -32602 with data { uri }, as 2026-07-28 requires. if (!note) throw new ResourceNotFoundError(uri.href); return { contents: [{ uri: uri.href, text: `# ${note.title}\n\n${note.body}` }] }; }, );
// A prompt: a message template the user picks, with arguments. server.registerPrompt( 'summarize-note', { title: 'Summarize a note', description: 'Ask the model for a one-sentence summary of a note.', argsSchema: z.object({ id: z.string().describe('Note id, such as welcome') }), }, ({ id }) => ({ messages: [ { role: 'user', content: { type: 'resource', resource: { uri: `notes://${id}`, text: notes[id]?.body ?? '', mimeType: 'text/markdown' } }, }, { role: 'user', content: { type: 'text', text: 'Summarize this note in one sentence.' } }, ], }), );
// A tool, so the model can find notes on its own. server.registerTool( 'search_notes', { description: 'Return the ids of notes whose title or body contains the query.', inputSchema: z.object({ query: z.string() }), outputSchema: z.object({ ids: z.array(z.string()) }), }, async ({ query }) => { const q = query.toLowerCase(); const ids = Object.keys(notes).filter((id) => `${notes[id].title} ${notes[id].body}`.toLowerCase().includes(q)); return { content: [{ type: 'text', text: JSON.stringify({ ids }) }], structuredContent: { ids } }; }, );
return server;});console.error('notes-ts: ready on stdio');Resources
Section titled “Resources”registerResource takes a name, a URI or a template, metadata, and a read callback.
- A fixed URI such as
notes://indexis one resource with one piece of content. - A
ResourceTemplatesuch asnotes://{id}covers many URIs. The read callback gets the variables ({ id }). Thelistcallback is required in the constructor, even if you passundefined; when you provide it,resources/listshows every concrete resource it returns.
npx -y @modelcontextprotocol/inspector@2.12.0 --cli \ node src/server.ts --protocol-era auto --method resources/listExpectednotes://index, notes://welcome and notes://release.
npx -y @modelcontextprotocol/inspector@2.12.0 --cli \ node src/server.ts --protocol-era auto --method resources/read --uri notes://welcomeExpectedThe note text, plus ttlMs and cacheScope.
--method resources/templates/list shows the template itself, notes://{id}.
The 2026-07-28 result also carried "ttlMs": 0 and "cacheScope": "private". These are cache hints for the client; the SDK fills in these defaults. To set your own, pass cacheHint: { ttlMs, cacheScope } in the resource’s metadata.
A resource that does not exist
Section titled “A resource that does not exist”Throw ResourceNotFoundError with the URI:
if (!note) throw new ResourceNotFoundError(uri.href);The 2026-07-28 spec changed the error for a missing resource to -32602 (Invalid Params) with the URI in data. We read notes://missing from the TypeScript client SDK on both protocol generations and got the same error each time: code -32602, data {"uri":"notes://missing"}, message Resource not found: notes://missing. Older servers used -32002; the SDK no longer sends it, but its clients still understand it.
If you throw a plain Error instead, the client still gets an error, but it cannot tell “not found” apart from any other failure.
Prompts
Section titled “Prompts”registerPrompt takes a name, a config with an optional argsSchema, and a callback that returns messages.
npx -y @modelcontextprotocol/inspector@2.12.0 --cli \ node src/server.ts --protocol-era auto \ --method prompts/get --prompt-name summarize-note --prompt-args id=releaseExpectedTwo user messages: the note as an embedded resource, then the instruction.
In prompts/list, the argument appears as id with required: true and the description from .describe(). The host shows this to the user, so write the description for a person, not for the model.
A message can embed a resource (type: 'resource') instead of pasting its text. The host then knows where the content came from.
When to use which
Section titled “When to use which”| You want… | Use |
|---|---|
| The model to do something or look something up on its own | A tool |
| The host to show or attach data the user chooses | A resource |
| The user to start a task from a menu | A prompt |
Resources are controlled by the host application and prompts by the user, so each host decides whether and how it shows them. If the data must reach the model no matter which client is used, also offer a tool that returns it, like search_notes here.
Sources
MCPDev is community documentation. It is not affiliated with the Model Context Protocol project or with the vendors of the clients it covers.