MCP Resource Patterns
Resources are read-only, URI-addressable data. Get them right and your server feels like a filesystem the agent can browse.
When to Use
- Deciding whether a read operation should be a tool or a resource
- Exposing hierarchical data (files, tickets, logs) to an agent
- Implementing live-updating data via subscriptions
- Designing URI schemes for a new MCP server
Tool vs Resource vs Prompt
| Aspect | Tool | Resource | Prompt |
|---|---|---|---|
| Invocation | Agent calls it | Agent reads it | User selects it |
| Side effects | Allowed | No | No |
| Addressing | Name + args | URI | Name |
| Result | Agent-chosen | User/agent-attached | Template expansion |
| Example | create_issue |
github://issues/123 |
/summarize-pr |
Rule of thumb: if the agent needs to decide whether to read it, it's a tool. If the user or agent needs to attach specific known data to context, it's a resource.
Static Resources
server.resource(
"changelog",
"file://changelog.md",
async () => ({
contents: [{
uri: "file://changelog.md",
mimeType: "text/markdown",
text: await fs.readFile("CHANGELOG.md", "utf-8"),
}],
}),
);
Templated Resources
Use URI templates (RFC 6570) for addressable collections:
server.resource(
"issue",
"github://repos/{owner}/{repo}/issues/{number}",
async (uri, { owner, repo, number }) => {
const issue = await gh.issues.get({ owner, repo, issue_number: +number });
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(issue.data),
}],
};
},
);
The client auto-discovers the template and can fetch any matching URI.
Listing Resources
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{ uri: "github://repos/acme/api/issues", name: "Open issues", mimeType: "application/json" },
{ uri: "github://repos/acme/api/prs", name: "Open PRs", mimeType: "application/json" },
],
}));
Keep the list short — this is what the user sees in the resource picker.
Subscriptions (Live Data)
For data that changes (logs, metrics, ticket state), support subscriptions so the client re-reads on update:
server.resource(
"live-log",
"logs://app/{stream}",
{ subscribe: true },
async (uri, { stream }) => ({ contents: [{ uri: uri.href, text: await tail(stream) }] }),
);
// When data changes:
logStream.on("update", (stream) => {
server.sendResourceUpdated({ uri: `logs://app/${stream}` });
});
The client listens for notifications/resources/updated and re-fetches.
URI Scheme Design
A good scheme is:
- Hierarchical —
github://repos/{owner}/{repo}/issues/{number}, notgithub://issue?id=123&repo=... - Guessable — if the agent knows
github://repos/acme/api/issues/5, it can guess.../issues/6 - Unique per server — prefix with your server's identity (
github://, notresource://) - MIME-tagged — always include
mimeTypeso the client renders correctly
Avoid opaque IDs in URIs when human-readable keys exist — linear://issues/ENG-42 beats linear://issues/a3f8c21d.
Binary Resources
return {
contents: [{
uri: uri.href,
mimeType: "image/png",
blob: base64Encode(pngBytes),
}],
};
Clients that support vision models will attach PNGs directly. Text-only clients will skip binary resources.
Pagination
MCP has no native pagination. Two conventions:
- Sub-resources:
github://issues?page=2as a distinct URI - Cursor in body: return JSON with
nextPageUriand let the agent fetch it
Prefer (1) — clients cache per-URI.
Common Mistakes
- Using tools for reads —
get_issueshould begithub://issues/{number}so the user can attach it as context - Using resources for actions — a resource read must not mutate state, ever
- No
mimeType— breaks rich clients and model understanding - Templates with too many variables — 5+ vars means you probably need a search tool instead
- Unstable URIs — if
github://issues/123points to different data tomorrow, caching breaks
Best Practices
- Design URIs as if they'll be bookmarked — stable, human-readable, hierarchical
- Expose bulk endpoints as resource listings, detail endpoints as templated resources
- Keep resource lists small (< 50 items); let templates handle the long tail
- Use subscriptions sparingly — each one holds a connection open
- Document your URI scheme in the server README — users browse before they call