# Model Context Protocol
An application annotates it, and the methods it names as tools are what a model may call.
```yaml
exposition:
mcp:
name: Teapots
instructions: A pot is read by its id. A pot that is brewing cannot be emptied.
origins: [https://claude.ai]
```
name
- Required. What the server calls itself, which a client shows a user.
instructions
- What a model is told before it calls anything here.
origins
- Browser origins the endpoint answers. A request carrying an
Origin that is not
listed is 403, and an empty list admits none; a request carrying none is unaffected.
anonymous
- Whether the endpoint answers without a credential. It does not unless it says so.
Without the annotation `/.mcp` is a path like any other, and nothing answers there. The path is
fixed: one an application could choose is one it could collide with a route of its own.
## What a tool is
A tool is an RTD method that says it is one, named as the [procedure](rpc.md#the-name) it is:
```yaml
/pots:
GET:
mcp:tool: Every pot there is, newest first.
endpoint: enumerate
/hot:
GET:
query: { criteria: temperature=gt=80 }
mcp:tool: The pots that are too hot to pour.
endpoint: enumerate
```
A default denies, and a tree holds everything an application serves — its identity endpoints, its
uploads, the machinery of the authorization flow the model already came through. Publishing all of
it would spend a model's context on what it has no business calling.
The value is what the tool is, and stating it is what publishes it — there is no way to publish one
that says nothing, because a tool a model cannot read the purpose of is one it cannot choose.
An operation [states what it is](/documentation/component/declaration.md) as well, and that is not
this: it is written without knowledge of any route, and a tool is an operation and a route together.
The two routes above are one operation and two tools, and one sentence is not true of both. The
operation's own is the Introspection's to read, and the gateway does not use it.
A declaration is inherited by everything below it, as every directive is, and the nearer one wins.
Since what it carries is what one method is, it belongs on a method: a node stating one would say
the same thing of everything under it.
A route whose name a client [cannot spell](rpc.md#what-has-a-name) is refused where the directive
is built, rather than served as a tool that is quietly never listed.
`tools/list` answers every published method this caller may reach — a method whose directives refuse
them is not listed, and a route with [no name](rpc.md#what-has-a-name) is not either. `tools/call`
answers `404` to a name that is not published, whether or not a route would have taken it: what an
application did not publish is not reachable by guessing what it would have been called.
`annotations` are read from the verb: `GET` and `HEAD` are `readOnlyHint`, `DELETE` is
`destructiveHint`, `PUT` and `DELETE` are `idempotentHint`.
## What a tool takes
`inputSchema` is the procedure's `params`, which is what the method
[says of itself](introspection.md): a route variable by the name the template gives it, the
querystring under `query`, and what is left is the body. `io:input` restricts it, and a property
`map` fills is not there — a call carries no headers of its own.
```yaml
type: object
properties:
id: { type: string, pattern: ^[a-fA-F0-9]{32}$ }
query:
type: object
properties:
criteria: { type: string }
sort: { type: string }
limit: { type: integer, minimum: 1, maximum: 100, default: 10 }
omit: { type: integer, minimum: 0, maximum: 1000, default: 0 }
required: [id]
additionalProperties: false
```
## What a tool answers
`content` carries one `text` block holding the reply's JSON, and `structuredContent` the reply
itself. A reply of nothing is an empty `content`.
`outputSchema` is what the operation declares, restricted by `io:output`. An operation's `output` is
optional and normalizes to `{}`, which describes nothing, and none is stated for it — an
application that wants the schema declares the operation's `output`.
An operation that refuses answers a result with `isError: true` carrying its message, not an error
of the protocol: it is something a model reads and may correct itself by.
## Two revisions
Both are answered from one endpoint that remembers nothing between requests.
[`2026-07-28`](https://modelcontextprotocol.io/specification/2026-07-28) carries the version and
the client's capabilities in every request; `2025-11-25` opens with `initialize`. Which one a
request is, is read from `MCP-Protocol-Version`, or from
`_meta['io.modelcontextprotocol/protocolVersion']` where the header is absent; `initialize` is one
by itself, and a request naming no version is of a revision that sent none.
| method | |
| --- | --- |
| `server/discover` | what is served, and who serves it |
| `tools/list` | every tool this caller may reach, in a stable order |
| `tools/call` | the call the tool is |
| `initialize` | the same as `server/discover`, in the shape a client of `2025-11-25` reads |
| `notifications/initialized` | `202`, and nothing done |
| `ping` | `{}` |
`POST` only, one message per request; `GET` and `DELETE` are `405`. An `Mcp-Session-Id` is ignored
and none is minted, a `Last-Event-ID` is ignored, and `accept` is negotiated as everywhere else.
Of the modern revision, `MCP-Protocol-Version` and `Mcp-Method` are required, and `Mcp-Name` for a
`tools/call`; each must say what the body says. `_meta` states the protocol version and the client's
capabilities. Every result carries `resultType` and names the server in its own `_meta`, and a
`server/discover` or a `tools/list` carries how long it may be held: half an hour, `public` for the
first and `private` for the second, which is filtered by the identity that asked.
Neither `listChanged` nor a subscription is declared: both are a stream held open, and this endpoint
holds none.
## Authorization
The endpoint is a protected resource, and an application advertises it as one:
```yaml
exposition:
oauth:
authorize: https://app.example.com/oauth/authorize
resources: ['/.mcp']
```
Its RFC 9728 document is then read at `/.well-known/oauth-protected-resource/.mcp`, and the
canonical URI of the resource is `https://api.example.com/.mcp`. See [OAuth](oauth.md).
A request without a credential is `401`, carrying the challenge that names that document — which is
where the flow starts. A call the identity is not authorized to make is `403` with
`error="insufficient_scope"`. Each tool is authorized as the resource it is, against the path and
the verb its name states.
A route that is [`anonymous`](access.md#anonymous) is reached here whatever the client presented:
the rule that refuses a credentialed request is about a cacheable reply, and what a tool answers is
not one. So a resource that is public over HTTP is a public tool, and nothing in a manifest has to
say it twice.
## What refuses
| | |
| --- | --- |
| `-32700` | the body is not readable |
| `-32600` | the body is not one JSON-RPC message |
| `-32601` | no method of that name, at `404` |
| `-32602` | the arguments do not fit, or `_meta` states too little |
| `-32603` | anything the gateway did not mean to answer |
| `-32020` | a header says one thing and the body another |
| `-32022` | a revision neither served, naming both |
The codes MCP reserves for itself are `-32020` to `-32099`, and it has an implementation use none of
`-32000` to `-32019` — which is where [JSON-RPC](rpc.md#what-refuses) has this gateway's own, so none
of those is answered here.
## References
- [The specification](https://modelcontextprotocol.io/specification/2026-07-28)
- [JSON-RPC](rpc.md), where a procedure gets its name
- [Introspection](introspection.md), which is what a tool's schemas are
- [Features](../features/mcp.feature)