Skip to content

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.

src/server.ts
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');

registerResource takes a name, a URI or a template, metadata, and a read callback.

  • A fixed URI such as notes://index is one resource with one piece of content.
  • A ResourceTemplate such as notes://{id} covers many URIs. The read callback gets the variables ({ id }). The list callback is required in the constructor, even if you pass undefined; when you provide it, resources/list shows every concrete resource it returns.
TerminalList resources
Terminal window
npx -y @modelcontextprotocol/inspector@2.12.0 --cli \
node src/server.ts --protocol-era auto --method resources/list

Expectednotes://index, notes://welcome and notes://release.

TerminalRead one
Terminal window
npx -y @modelcontextprotocol/inspector@2.12.0 --cli \
node src/server.ts --protocol-era auto --method resources/read --uri notes://welcome

ExpectedThe 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.

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.

registerPrompt takes a name, a config with an optional argsSchema, and a callback that returns messages.

TerminalGet the prompt with an argument
Terminal window
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=release

ExpectedTwo 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.

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.