Server Primitives: Tools, Resources, Prompts
Module 4 — Server Primitives: Tools, Resources, Prompts
Phase 2 · The Primitives & Building. Phase 1 was anatomy; now you design real capabilities. A server exposes its power through exactly three primitives — tools, resources, prompts. The deep insight of this module is that they differ not by what they do but by who controls them. Get that control triad right and every “where should this capability live?” question answers itself — including the read/write split that becomes your entitlement boundary in Phase 3.
By the end you can:
- Place any capability on the model / app / user control axis and justify it.
- Design tool schemas that are reliable and safe (tight inputs, structured outputs, honest annotations).
- Explain resources (URIs, templates, subscriptions) and prompts (arguments, embedded resources).
- Identify the read-vs-write split and why it’s a security boundary, not just a category.
1. The control triad — the one idea to remember
| Primitive | Controlled by | Analogy | Side effects? | Invoked when |
|---|---|---|---|---|
| Tools | Model | POST / function call | Yes (often) | The LLM decides it needs to act |
| Resources | Application (host) | GET / file read | No (read-only) | The host loads context |
| Prompts | User | Slash command / template | n/a | The user explicitly picks it |
Every design decision flows from this. “Should fetching a customer record be a tool or a resource?” → Is the model deciding to fetch it mid-reasoning (tool), or is the host preloading it as context the user selected (resource)? “Should this be a prompt?” → Is the user explicitly invoking a reusable template? If yes, prompt.
Memorize it as model / app / user. It’s the spine of the whole module.
2. Tools — model-controlled actions
A tool is a function the model can choose to call. Tools are where side effects live: create an issue, send an email, run a query, write a file. Discovery is tools/list; invocation is tools/call (you saw both in Module 2).
Anatomy of a tool definition
{
"name": "create_issue", // unique id the model calls
"title": "Create Issue", // human-friendly label (optional)
"description": "Create a new issue in the given project. Use only for new work items.",
"inputSchema": { // JSON Schema — how the model builds arguments
"type": "object",
"properties": {
"project": { "type": "string", "description": "Project key, e.g. 'ENG'." },
"title": { "type": "string" },
"priority": { "type": "string", "enum": ["low", "medium", "high"] }
},
"required": ["project", "title"]
},
"outputSchema": { // optional — typed result shape
"type": "object",
"properties": { "id": { "type": "string" }, "url": { "type": "string" } },
"required": ["id"]
},
"annotations": { // hints to the HOST about behavior
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": true
}
}
Why each field is a reliability and security surface
description— the model acts on this text. Vague descriptions cause wrong calls; malicious descriptions can carry prompt injection (you’ll see “tool poisoning” in Module 8). Write them precise, scoped, and honest. The description is part of your attack surface.inputSchema— tight schemas mean fewer malformed calls. Preferenumover free-form strings, constrain types and ranges, markrequired. The model fills arguments from this schema; loose schemas → unreliable calls and a wider injection surface.outputSchema+ structured content — return typed data (astructuredContentobject), not just a blob of text, so the host and model get machine-readable results.2025-06-18formalized structured tool output; use it.annotations— hints (not security guarantees) that help the host reason about risk:readOnlyHint— does not modify state.destructiveHint— may delete/overwrite (e.g.delete_repo).idempotentHint— calling twice == calling once.openWorldHint— touches external/unbounded systems (the internet, third-party APIs).
Annotations are advisory. A host may use
destructiveHintto require extra confirmation, but it must never trust a server’s self-reportedreadOnlyHintas a security control — enforcement belongs to the host/gateway (Phase 3). Hints guide UX; entitlements enforce policy.
Tool results recap (from Module 2)
A result carries a content array of typed blocks (text, image, audio, resource_link, embedded resource) and/or structuredContent, plus isError. Remember: a failed tool returns a successful response with isError: true — the model reads and recovers.
The read/write split — your future entitlement boundary
Sort every tool into read (readOnlyHint: true, no side effects — get_issue, search) vs write/destructive (create_issue, delete_issue, send_email). This isn’t bookkeeping:
- Writes deserve stricter consent, stricter entitlements, and mandatory audit.
- In Phase 3 you’ll grant roles read tools liberally and write/destructive tools narrowly (deny-by-default).
- A clean split also makes scopes map cleanly (
issues:readvsissues:write).
Design tools so a single tool is either read or write, never both — it keeps the boundary crisp.
3. Resources — application-controlled data
A resource is read-only data identified by a URI. Think “GET, no side effects.” The host decides what to load into context — maybe the user attached a file, maybe the app preloads a doc. The model doesn’t autonomously “call” a resource the way it calls a tool.
Key methods and shape
resources/list→ enumerate concrete resources. Each hasuri,name, optionaltitle,description,mimeType,size.resources/read→ return contents: a list of items each withuri,mimeType, and eithertext(text resources) orblob(base64 for binary).- URI schemes are server-defined:
file:///logs/app.log,db://orders/4521,https://…, custommyapp://….
Resource templates (parameterized resources)
For families of resources, servers expose URI templates (RFC 6570) via resources/templates/list:
{ "uriTemplate": "db://customers/{customerId}",
"name": "Customer record", "mimeType": "application/json" }
The host fills {customerId} to read a specific one. Templates are how you expose “any row” without listing millions of URIs.
Subscriptions & dynamism
If the server advertises resources: { subscribe: true }, a client can resources/subscribe to a URI and receive notifications/resources/updated when it changes. resources: { listChanged: true } promises notifications/resources/list_changed when the set changes (recall the listChanged pattern from Module 3).
Design guidance: put reference material the host/user chooses to include in resources (docs, records, logs, schemas). Put actions and on-demand fetches the model decides to make in tools. If you find yourself wanting the model to autonomously “read a resource mid-reasoning,” that’s often a sign it should be a (read) tool instead — because tools are model-controlled and resources are app-controlled.
4. Prompts — user-controlled templates
A prompt is a reusable, parameterized message template the user explicitly invokes — often surfaced as a slash command or menu item (“/summarize”, “/code-review”). Prompts package expertise: a good prompt encodes the right instructions, structure, and context so the user doesn’t have to.
Methods and shape
prompts/list→ available prompts, each withname, optionaltitle,description, and anargumentslist (each arg:name,description,required).prompts/get(withname+arguments) → returns amessagesarray (role + content) ready to feed the model. Messages can embed resources, so a prompt can pull in data as part of its template.
// prompts/get result
{ "description": "Review a diff for bugs and style.",
"messages": [
{ "role": "user",
"content": { "type": "text",
"text": "You are a senior reviewer. Review this diff for correctness, security, and style. Diff:\n{{diff}}" } }
] }
Argument completion (nice-to-have)
Servers can support completion/complete to offer autocomplete for prompt (and resource-template) arguments — e.g. suggest valid project keys as the user types. Improves the UX of user-invoked prompts.
Design guidance: prompts are for user-initiated, repeatable workflows. If the model should decide when to do something, it’s a tool. If the host preloads data, it’s a resource. If the user picks a canned workflow, it’s a prompt. Same triad, every time.
5. Choosing the right primitive (worked examples)
| Capability | Primitive | Why |
|---|---|---|
| ”Create a Jira ticket” | Tool (write) | Model decides to act; side effect; needs strict entitlement |
| ”Search tickets” | Tool (read) | Model decides to fetch mid-reasoning; no side effect |
| ”Attach this design doc to context” | Resource | Host/user chooses to include read-only data |
| ”Any customer record by id” | Resource template | Parameterized read-only data family |
| ”/triage-bug workflow” | Prompt | User explicitly invokes a reusable template |
| ”Send a Slack message” | Tool (write, destructive-ish) | Model-initiated action with side effects → consent + audit |
When two seem plausible, ask “who is in control of invoking this — model, app, or user?” That single question resolves it.
6. Milestone exercise
Design (schemas/definitions only — no implementation) a server for a ticketing system:
- ≥2 tools: at least one read and one write. Give each a tight
inputSchema(use anenumsomewhere), anoutputSchema, and honestannotations. - 1 resource (or resource template): something the host would load as context.
- 1 prompt: a user-invoked workflow with at least one argument.
- For each of the above, write one sentence placing it on the model / app / user axis.
- Mark which tools are read vs write and write one sentence on how you’d entitle each differently in Phase 3.
Self-check:
- Your write tool has
readOnlyHint: false(anddestructiveHintset honestly); your read tool hasreadOnlyHint: true. - At least one schema uses
enumand marksrequiredfields. - You used a tool (not a resource) for anything the model decides to invoke, and a resource for anything the host/user preloads.
- Your prompt is genuinely user-initiated, not something the model would trigger.
- You can state, in one line, why annotations are hints and not enforcement.
Nail the read/write split — it’s the seam everything in Phase 3 hangs on. Next: Module 5 — Client primitives (Roots, Sampling, Elicitation), the capabilities that flow the other direction.
Quick reference — Module 4 glossary
- Control triad — Tools = model-controlled, Resources = app-controlled, Prompts = user-controlled.
- Tool — model-invoked function;
name,description,inputSchema, optionaloutputSchema,annotations;tools/list+tools/call. - Annotations — advisory hints (
readOnlyHint,destructiveHint,idempotentHint,openWorldHint); guide UX, never enforce security. - Structured content — typed tool output via
outputSchema/structuredContent. - Resource — URI-identified read-only data;
resources/list/read; templates (RFC 6570); optionalsubscribe+listChanged. - Prompt — user-invoked template;
prompts/list/get; arguments + embeddable resources; optional argument completion. - Read/write split — the per-tool boundary that becomes the entitlement & audit boundary in Phase 3.