intermediate
ai-engineering

Resources & Prompts

Expose data resources and reusable prompt templates through MCP servers

mcp fundamentals
28 min read

Resources and Prompts

Tools let a model do things, but a capable MCP server also needs to offer context and guide interactions. That is what the other two primitives are for. Resources expose read-only data — files, database rows, API responses — that a client can pull into the model's context. Prompts are reusable, parameterised message templates that surface in the client UI (often as slash commands) so users trigger well-crafted interactions without retyping them. Getting these right is what turns a toy server into something a team actually integrates into Claude Desktop, an IDE, or a custom agent.

Learning Objectives
  • Distinguish tools, resources, and prompts by who controls them
  • Register static resources and read their contents
  • Build dynamic resources with URI templates
  • Notify clients when resource data changes
  • Define prompt templates with typed arguments

Three Primitives, Three Controllers

MCP defines three server primitives. The key mental model is who decides when each one is used:

PrimitiveControlled byAnalogous toSide effects
ToolsThe modelPOST endpointsYes — actions
ResourcesThe applicationGET endpointsNo — read-only
PromptsThe userSlash commands / macrosNo — templates

Before a client will request any of these, declare the capabilities during construction. listChanged tells the client you will emit list-update notifications; subscribe enables per-resource change subscriptions.

typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js" const server = new Server( { name: "docs-server", version: "1.0.0" }, { capabilities: { resources: { subscribe: true, listChanged: true }, prompts: { listChanged: true }, }, }, )

Exposing Static Resources

A resource is identified by a URI and described with a name, optional description, and a mimeType. Listing resources advertises what exists; reading one returns its contents. The high-level McpServer API wires both handlers for you, while the low-level Server gives you full control over each JSON-RPC method.

typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" import { readFile } from "node:fs/promises" const server = new McpServer({ name: "docs-server", version: "1.0.0" }) server.registerResource( "api-docs", // internal id "file:///docs/api.md", // stable URI { title: "API Reference", description: "REST endpoint docs", mimeType: "text/markdown" }, async (uri) => ({ contents: [ { uri: uri.href, mimeType: "text/markdown", text: await readFile("docs/api.md", "utf8") }, ], }), )

The contents array can hold either text or binary entries. For binary payloads, drop text and send a base64 blob alongside its mimeType:

typescript
return { contents: [ { uri: "file:///logo.png", mimeType: "image/png", blob: pngBuffer.toString("base64") }, ], }

Dynamic Resources with URI Templates

Enumerating every possible resource is impractical when data is keyed by an id or date. Resource templates advertise a parameterised URI using RFC 6570 syntax, and the SDK extracts the parameters for you on read.

typescript
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js" server.registerResource( "user-profile", new ResourceTemplate("users://{userId}/profile", { list: undefined }), { title: "User Profile", description: "Public profile as JSON" }, async (uri, { userId }) => ({ contents: [ { uri: uri.href, mimeType: "application/json", text: JSON.stringify(await getUser(userId)) }, ], }), )

With the low-level API you expose the template through its own list handler so clients can discover the pattern:

typescript
import { ListResourceTemplatesRequestSchema } from "@modelcontextprotocol/sdk/types.js" server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: [ { uriTemplate: "users://{userId}/profile", name: "User profile", mimeType: "application/json" }, ], }))

Notifying Clients When Data Changes

Static context goes stale. When the underlying data updates, emit a notification so clients can re-read. There are two levels: notifications/resources/list_changed (the set of resources changed) and notifications/resources/updated (one resource's contents changed). The latter requires subscribe capability and a subscribe handler.

typescript
import { SubscribeRequestSchema, UnsubscribeRequestSchema } from "@modelcontextprotocol/sdk/types.js" const subscribed = new Set<string>() server.setRequestHandler(SubscribeRequestSchema, async (req) => { subscribed.add(req.params.uri) return {} }) server.setRequestHandler(UnsubscribeRequestSchema, async (req) => { subscribed.delete(req.params.uri) return {} }) // Later, when a watched file changes on disk: async function onFileChanged(uri: string) { if (!subscribed.has(uri)) return await server.notification({ method: "notifications/resources/updated", params: { uri } }) }

Defining Prompt Templates

Prompts are user-invoked templates. Each has a name, an optional description, and typed arguments. prompts/list advertises them; prompts/get fills in the arguments and returns concrete messages the client sends to the model.

typescript
import { z } from "zod" server.registerPrompt( "code-review", { title: "Code Review", description: "Review a snippet for bugs and style", argsSchema: { code: z.string(), language: z.string().optional() }, }, ({ code, language }) => ({ messages: [ { role: "user", content: { type: "text", text: `Review this ${language ?? "code"} for bugs and style:\n\n${code}` }, }, ], }), )

Embedding Resources in Prompts

A prompt message is not limited to plain text. It can embed a resource, letting one template pull live data into the conversation. Use content type resource with the same contents shape returned by resources/read.

typescript
server.setRequestHandler(GetPromptRequestSchema, async (req) => ({ messages: [ { role: "user", content: { type: "text", text: "Summarise the current API docs." } }, { role: "user", content: { type: "resource", resource: { uri: "file:///docs/api.md", mimeType: "text/markdown", text: await readFile("docs/api.md", "utf8") }, }, }, ], }))

Best Practices & Common Pitfalls

Ship-ready checklist
  • Keep resources read-only. If it writes, it is a tool, not a resource.
  • Always set an accurate mimeType. Clients rely on it to render or parse contents; use application/json, text/markdown, etc.
  • Validate template parameters. A users://{userId} read is untrusted input — reject unknown ids instead of leaking errors.
  • Only advertise capabilities you implement. Declaring subscribe: true without a subscribe handler makes clients wait for updates that never arrive.
  • Mark required prompt arguments. Omitting required lets clients call prompts/get with missing data, forcing defensive fallbacks.

Next Steps

Continue Learning
Chain tools safely.

Next Lesson

Multi-Tool Orchestration