Tools, resources and prompts
- Spec
2026-07-28- Evidence
- E2Reproducedarm64
- TypeScript SDK (server)
@modelcontextprotocol/server@2.4.0- TypeScript SDK v2 (Node HTTP)
@modelcontextprotocol/node@2.1.2- Python SDK
mcp@2.3.0- Verified
A server can offer three kinds of things. The difference between them is who decides to use them:
| Primitive | Controlled by | Typical use |
|---|---|---|
| Tool | The model | Do something or compute something: add, search_orders, create_issue |
| Resource | The application | Data the host reads and puts into context: a file, a config, a database row |
| Prompt | The user | A template the person picks from a menu, such as “review this code” |
Pick by that question. If the model should decide when to call it, it is a tool. If the host should load it as context, it is a resource. If a person should choose it, it is a prompt.
All examples below come from one TypeScript server (SDK @modelcontextprotocol/server@2.4.0) that we ran over Streamable HTTP and queried with curl. The responses are trimmed but otherwise unchanged.
A tool has a name, a description, an inputSchema and, optionally, an outputSchema. Both schemas are JSON Schema; since 2026-07-28 they may use any JSON Schema 2020-12 keyword, as long as inputSchema has type: "object" at the root.
server.registerTool('add', { description: 'Add two numbers.', inputSchema: z.object({ a: z.number(), b: z.number() }), outputSchema: z.object({ sum: z.number() }),}, async ({ a, b }) => ({ content: [{ type: 'text', text: String(a + b) }], structuredContent: { sum: a + b },}));A call returns two things:
content: text, images or other blocks for the model and for clients that only display text.structuredContent: the machine-readable result. If the tool declares anoutputSchema, the result must match it. Since 2026-07-28 it can be any JSON value, not only an object.
In tools/list, the TypeScript SDK turned the Zod output schema into JSON Schema with "additionalProperties": false, so returning an extra field fails validation.
Resources
Section titled “Resources”A resource is data addressed by a URI. The server lists fixed resources with resources/list and parameterized ones with resources/templates/list:
server.registerResource('config', 'config://app', { title: 'App config', mimeType: 'application/json' }, async (uri) => ({ contents: [{ uri: uri.href, mimeType: 'application/json', text: '{"theme":"dark"}' }] }));
server.registerResource('note', new ResourceTemplate('notes://{id}', { list: undefined }), { title: 'A note by id', mimeType: 'text/plain' }, async (uri, { id }) => ({ contents: [{ uri: uri.href, text: `note ${id}` }] }));resources/list returned only config://app. The template appeared in resources/templates/list as "uriTemplate":"notes://{id}", and reading notes://42 returned:
{"contents":[{"uri":"notes://42","text":"note 42"}],"resultType":"complete","ttlMs":0,"cacheScope":"private"}The URI scheme is yours to choose. What matters is that the host can read the resource without the model deciding to call anything.
Prompts
Section titled “Prompts”A prompt is a named template with arguments. prompts/list shows the arguments and whether each is required; prompts/get fills them in and returns messages:
server.registerPrompt('review-code', { title: 'Code review', argsSchema: z.object({ code: z.string(), language: z.string().optional() }),}, ({ code, language }) => ({ messages: [{ role: 'user', content: { type: 'text', text: `Review this ${language ?? ''} code:\n${code}` } }],}));"arguments":[{"name":"code","required":true},{"name":"language","required":false}]Clients usually show prompts as slash commands or menu items. The server never runs them on its own.
Caching the lists
Section titled “Caching the lists”In 2026-07-28, results of tools/list, prompts/list, resources/list, resources/templates/list, resources/read and server/discover must carry two caching hints:
ttlMs: how long, in milliseconds, the client may treat the result as fresh.0means re-fetch whenever needed.cacheScope:"public"if the result has no user-specific data and a shared gateway may cache it;"private"if it may only be reused for the same authorization context.
The TypeScript SDK sends ttlMs: 0 and cacheScope: "private" unless you set hints. We set one for tools/list:
new McpServer({ name: 'primitives-demo', version: '1.0.0' }, { cacheHints: { 'tools/list': { ttlMs: 60000, cacheScope: 'public' } } });tools/list then returned "ttlMs":60000,"cacheScope":"public", while the other lists kept the defaults. prompts/get and tools/call carry no caching fields at all. In the Python SDK 2.3.0 the same option is cache_hints on MCPServer.
Only mark a list public if every user sees the same list. If tools depend on who is calling, keep it private.
Change notifications
Section titled “Change notifications”When the set of tools, prompts or resources changes, the server can notify clients that opted in. In 2026-07-28 that happens on a subscriptions/listen stream, not through a separate GET connection. Caching and notifications work together: the TTL says how long a list stays fresh, and a notification says it changed early.
Sources
MCPDev is community documentation. It is not affiliated with the Model Context Protocol project or with the vendors of the clients it covers.