Anatomy of a server: tools, resources, prompts

Lesson 1 of 5 in Building with MCP: Servers, Clients, and the Install Review.

An MCP server is not a framework, a model, or an agent. It is a program that publishes a surface — and under revision 2026-07-28 that surface has exactly three parts that servers offer to clients:

  • Tools — “functions for the AI model to execute”. Discovered with tools/list, invoked with tools/call.
  • Resources — “context and data, for the user or the AI model to use”. Discovered with resources/list, fetched with resources/read.
  • Prompts — “templated messages and workflows for users”. Discovered with prompts/list, fetched with prompts/get.

Every primitive has that same pair of verbs: one to enumerate, one to retrieve or execute. If you can hold that table in your head, you can read any MCP server’s source in ten minutes, because there is nowhere else for its capabilities to hide.

Key terms: MCP, MCP server, tool, function calling, structured outputs, JSON-RPC 2.0

Declaring a tool is two artefacts that must be designed together and are almost always reviewed apart.

The declaration is data — a name, a description, an inputSchema (JSON Schema; the 2020-12 dialect is the default when no $schema is present), an optional outputSchema, an optional title and icons, and optional annotations. This is the half the model reads. It is your entire user manual for a reader who will never see your code.

The handler is code — the function your server runs when a tools/call arrives. This is the half your security reviewer reads. Per the spec, servers MUST validate all tool inputs, implement access controls, rate-limit invocations, and sanitize outputs. The schema is a hint to the model about what to send, never a guarantee about what will arrive.

The three server features, and who is supposed to drive each
FeatureMeant to be driven byMethodsReach for it whenSymptom of picking wrong

Tools — functions for the model to execute

The model. Tools are model-controlled: the LLM discovers and invokes them inside the agent loop.

tools/list, tools/call

The agent must act or fetch on demand, with arguments it chooses at runtime.

Nothing — this is the default and correct choice for most server surfaces.

Resources — context and data

The application or the user, which selects what to attach; the model reads what lands in context.

resources/list, resources/read

There is a browsable, addressable body of content (files, records, dashboards) a user or host may want to attach by URI.

Exposing an action as a resource makes it invisible to the model as a capability — it can read, but it cannot decide to do.

Prompts — templated messages and workflows

The user, typically through a slash-command or menu in the host UI.

prompts/list, prompts/get

You are shipping an expert workflow — a review checklist, a triage script, a few-shot template — that a person invokes deliberately.

Burying a workflow in a tool description hopes the model reads it; a prompt puts it in the user’s hands where it is actually chosen.

1 · Name and title — routing, in two words

search_incidents tells the model which shelf the tool sits on before it reads a word of prose. Keep the verb honest: a tool named get_ that writes will be called in contexts where nobody expected a write. The optional title is for humans in the host UI; the name is the identifier the model emits.

2 · Description — the when, and the when not

The declaration is the only documentation the model ever reads, so the description carries the routing rules your README carries for humans: what this is for, what it returns, its limits, and explicitly what it is not for. Note the two negative clauses in the example above — “at most 25 matches” and “does not create, update, or close incidents.” Absent those, the model will eventually try to close an incident with a search tool and improvise the arguments.

3 · inputSchema — constrain rather than hope

enum for severity, minimum/maximum for the limit, a format example inside the parameter description. Every constraint you express in JSON Schema is one the client can validate and the model can read. Every constraint you leave in prose is one the model may ignore under non-determinism. And every constraint you express anywhere must still be re-validated in the handler, because the schema binds well-behaved clients, not attackers.

4 · outputSchema — a contract on the way back

Declaring outputSchema moves the return value from “some text the model must parse” to a validated JSON object, which is what makes tool results safe to chain into code rather than only into prose. Note the naming trap: MCP’s structuredContent is a tool result field and has nothing to do with an LLM API’s structured outputs feature. Different layer, similar words.

5 · Errors — two mechanisms, and the difference matters

MCP separates protocol errors (standard JSON-RPC errors — unknown tool, malformed request) from tool execution errors, reported inside a successful result with isError: true. The distinction is functional: execution errors SHOULD be passed to the model so it can self-correct and retry. Raise a protocol error for “no such incident” and you have hidden the failure from the only participant capable of fixing it.

6 · Annotations — hints for display and policy, not enforcement

Tool annotations describe behaviour — a display title plus behaviour hints. As of September 2026 the widely published hint fields are readOnlyHint, destructiveHint, idempotentHint and openWorldHint; confirm the exact field names against the schema for your target revision before you depend on them.

The rule you must not soften: the tools spec says clients MUST consider tool annotations to be untrusted unless they come from trusted servers. A readOnlyHint is a claim by the server about itself. It is a useful risk vocabulary and a terrible access control.

The good news for the implementation half: you will not be writing JSON-RPC framing by hand. There are ten official MCP SDKs, tiered by feature completeness, protocol support, and maintenance commitment — Tier 1: TypeScript, Python, C#, Go, Rust; Tier 2: Java, Ruby; Tier 3: Swift, PHP, Kotlin. All live under github.com/modelcontextprotocol/<lang>-sdk.

Tier is the number to check before you commit a team, because it predicts how quickly the SDK will support the revision you need. A Tier 3 SDK on a project that needs the newest revision on day one is a decision, not a detail.

Interactive checkpoint quiz (2 questions) — open this page in a browser to take it.