Let an AI Assistant Call the API
A customer asks their AI assistant where their order is, and the assistant looks it up in the shop. A language model acting for a user calls tools. The application’s routes are served as tools through the Model Context Protocol, from the same declarations that serve HTTP: publish a method, describe it, and a model can do what the user it acts for can do, under the same access rules.
§A first tool
Turn the endpoint on in the Context and give the server a name. mcp is added to exposition, beside the authorities that toa create wrote:
# context.toa.yaml
exposition:
authorities:
local: localhost:8000
mcp:
name: Store Publish a method as a tool and say what it is for. The routes are those of store.orders, as Expose It over HTTP declares them; the GET of one
order is written out to take the two directives:
# components/store.orders/manifest.toa.yaml
exposition:
/:
anonymous: true
io:output: true
GET: enumerate
POST: create
/:id:
io:output: [id, status, total]
GET:
mcp:tool: true
help:method:
title: An order
description: The status and total of one order, by its id.
endpoint: observe
PUT: approve The server is at POST /.mcp of the application’s host. A client that connects to https://api.store.example/.mcp lists one tool, store.orders._id.GET, and can call it.
Without the mcp annotation nothing answers at /.mcp. The path is fixed.
§Try it
For a first look without setting up OAuth, make the endpoint anonymous in the local environment
only. The route is anonymous too, by the anonymous: true on /:
# context.toa.yaml
exposition:
mcp:
name: Store
anonymous@local: true $ npm run restart Point a client at http://localhost:8000/.mcp; the discovery page shows the address to give it. Or ask as a client would, in a scenario. It needs one step the
generated features/steps/http.ts does not have; add it there:
// features/steps/http.ts
Then('the response does not contain:', function (this: World, text: string) {
this.agent.responseExcludes(text)
}) # features/tools.feature
Feature: Tools
Scenario: The order lookup is a tool, and nothing else is
When the request is sent:
"""
POST /store/orders/ HTTP/1.1
content-type: application/yaml
accept: application/yaml
customer: b70a7dbca6b14a2eaac8a9eb4b2ff4db
items: []
total: 4200
"""
Then the response is received:
"""
201 Created
id: ${{ order.id }}
"""
When the request is sent:
"""
POST /.mcp HTTP/1.1
accept: application/yaml
content-type: application/json
{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}
"""
Then the response is received:
"""
200 OK
- name: store.orders._id.GET
"""
And the response does not contain:
"""
store.orders.POST
"""
When the request is sent:
"""
POST /.mcp HTTP/1.1
accept: application/yaml
content-type: application/json
{"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "store.orders._id.GET", "arguments": {"id": "${{ order.id }}"}}}
"""
Then the response is received:
"""
200 OK
structuredContent:
id: ${{ order.id }}
status: pending
""" A scenario starts the components itself and sends its requests to the gateway, which is part of sys; see Write a Test:
$ npm run env
$ npm run sys
$ npm run features §Publishing methods
Nothing is a tool unless it says so. A tree holds everything an application serves — sign-in endpoints, uploads, internal machinery — and a model chooses among tools by reading their descriptions. A short list of well-described tools serves it better than every route there is.
mcp:tool: true publishes a method. Like every directive it is inherited, and the nearest
declaration wins, so a node can publish a subtree and one method can opt out:
exposition:
/:
mcp:tool: true
io:output: [id, status, total]
GET:
help:method: Orders, newest first.
endpoint: enumerate
/:id:
GET:
help:method: One order, by its id.
endpoint: observe
PUT:
mcp:tool: false
endpoint: approve §Describing a tool
mcp:tool only says that the method is published. What the tool is called and what it is for
comes from help:method, the same declaration that
describes the method to people:
help:method:
title: Pending orders
description: Orders waiting for approval, oldest first. Approve one with its id. titleis what a person sees where a client lists the tools.descriptionis what the model reads to decide whether to call it.
Always declare help:method beside mcp:tool. A tool whose purpose a model cannot read is
one it cannot choose. Write descriptions for a reader who knows nothing about the application:
say what comes back, what the identifiers are, and what to call next.
Describe parameters the same way, with help:route and help:query.
§Names
A tool’s name is the name of its procedure: store.orders.GET, store.orders._id.GET. A route that has no such name cannot be a tool, and declaring mcp:tool on it is refused when the manifest is read.
§What a tool takes
A tool’s input schema is derived from the method:
- a route variable, under the name the route gives it;
query, holding what the method lets a request select —criteria,sort,limit,omit,search— and the parameters it declares;- the properties of the operation’s input, restricted by
io:input.
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 A property that a map directive fills is not in the schema: a tool call
carries no headers of its own.
The schema is the operation’s contract, so the effort put into input schemas pays twice. Constraints and descriptions written there are what the model is held to.
A route with fixed criteria offers the model less to get wrong. /pending with criteria: status==pending is a better tool than / with instructions on how to filter.
§What a tool answers
The result carries the reply twice: as structuredContent, the reply itself, and as one text block of content holding its JSON. MCP allows structured content to be an object only, so a list
or a single value is the result of one: { "result": [...] }. A reply of nothing is an empty content.
The output is restricted by io:output, as the route’s is.
A tool’s outputSchema is the operation’s declared output, restricted the same way. An
operation that declares no output gets none; declare the operation’s output if clients should
have the schema. An output that is not an object, such as the array a list declares, is stated
as the result of one.
An operation that refuses — returns one of its declared errors — answers a result with isError: true carrying the error. This is not a protocol failure: a model reads it and may
correct itself. Error codes that say what to do differently (NOT_PENDING, INSUFFICIENT_FUNDS) are therefore worth more than generic ones.
§Hints
Clients treat tools differently depending on what they may do — many let a model call read-only tools without asking the user. The hints are derived from the HTTP method:
| Method | Hint |
|---|---|
GET, HEAD | readOnlyHint |
DELETE | destructiveHint, idempotentHint |
PUT | idempotentHint |
Because a GET may not write, readOnlyHint is true of
what the tool can do, not just of what it claims.
§Instructions
instructions is text a model is given before it calls anything. Use it for what no single tool
description can say: where to start, how tools follow one another, what the identifiers mean.
# context.toa.yaml
exposition:
mcp:
name: Store
instructions: >-
Orders belong to a customer. List orders with `store.orders.GET`,
read one with `store.orders._id.GET`.
An order can be approved only while its status is `pending`. Naming tools by their exact names in the instructions helps a model find the way.
§Who may call
A tool call is the call its route makes, by the same identity, under the same directives.
tools/listlists the published tools this caller may reach. A tool the caller’s identity would be refused is not listed.tools/callfor a name that is not published is404, whether or not a route would have taken it. What the application did not publish cannot be reached by guessing its name.- A call the identity is not authorized to make is
403.
§Credentials
By default the endpoint requires a credential: a request without one is 401, with a challenge
that tells the client where to obtain a token. MCP clients obtain one through OAuth, with the
user’s consent; the endpoint is advertised as a protected resource in the Context:
# context.toa.yaml
exposition:
oauth:
authorize: https://store.example/account/allow/
resources: ['/.mcp']
mcp:
name: Store Setting up the authorization server, consent and scopes is covered in The OAuth Server.
To let the endpoint answer without a credential — for tools over public data — say so:
exposition:
mcp:
name: Store
anonymous: true Each tool is still authorized as its route: anonymous: true on the server opens the door, and
only tools whose routes admit an anonymous caller are listed and callable. A route that is
public over HTTP is a public tool; nothing has to be said twice.
§Browser clients
A request from a browser carries an Origin. The endpoint answers only the origins the Context
lists:
exposition:
mcp:
name: Store
origins: [https://claude.ai] A request with an Origin that is not listed is 403; with no origins declared, no browser
origin is admitted. A request without an Origin — any client that is not a browser page — is
not affected.
§A host of its own
Users give their client an address. https://mcp.store.example is easier to hand out than https://api.store.example/.mcp. hosts names such a host per authority. local is the name the generated Context gives its one
authority, here with the host it has in production:
# context.toa.yaml
exposition:
authorities:
local: api.store.example
mcp:
name: Store
hosts:
local: mcp.store.example POST /of that host is the endpoint. It answers whatPOST /.mcpof the authority’s own host answers, which keeps working.- Nothing else is served there: the application’s routes,
/.rpc,/.mcpand/.discoveryare404, andGET /is405. - The host belongs to the authority it is declared under: identities and credentials are the
same, and
map:authorityyields the same identifier. - A key of
hoststhat names no declared authority fails the deployment. - The host is exposed by the gateway’s ingress, like the authority’s own.
- A host of its own is a protected resource without being listed in
oauth.resources.
Describe the API reports the host as mcp, and the discovery page
offers it as the address to give a client.
§Icons
A client that shows an icon for the server is given the icons of the application’s web manifest:
exposition:
mcp:
name: Store
manifest: https://store.example/manifest.json - The URL is
https, orhttpon a loopback host. - Only icons are taken from it. The server’s name is
name. - An icon is used if it is served from the manifest’s own origin, its
purposeis absent or includesany, and itstype, where stated, is an image. - The manifest is read when the gateway starts and every half hour after; a changed icon reaches clients within an hour. Redirects are not followed, and a manifest over 16 KiB is not read.
- If the manifest cannot be read the server has no icons, or keeps the last ones it read. The endpoint works either way.
§The protocol, briefly
Most developers never look below this line; an MCP client library does. For those who do:
- One endpoint,
POSTonly, one JSON-RPC message per request.GETandDELETEare405. - No sessions and no streams:
Mcp-Session-IdandLast-Event-IDare ignored, and neitherlistChangednor subscriptions are offered. - Two protocol revisions are answered:
2026-07-28, where every request carries its version, and2025-11-25, which opens withinitialize. The revision is read from theMCP-Protocol-Versionheader, or from_metaof the message.
| Method | Answers |
|---|---|
server/discover | What is served and by whom. |
initialize | The same, for a client of 2025-11-25. |
notifications/initialized | 202. |
tools/list | The tools this caller may reach, in a stable order. |
tools/call | The call. |
ping | {}. |
server/discover and tools/list may be held by a client for half an hour, so a newly
published tool can take that long to appear in a connected client.
Protocol errors:
| Code | Meaning |
|---|---|
-32700 | The body is not readable. |
-32600 | The body is not one JSON-RPC message. |
-32601 | No such method, or no such tool. Status 404. |
-32602 | The arguments do not fit the tool’s schema. |
-32603 | An exception. |
-32020 | A header and the body disagree. |
-32022 | A protocol revision that is not served. |
The reasoning behind tools is in Procedures and Tools.
§Reference
exposition.mcp in the Context:
| Key | Meaning | Default |
|---|---|---|
name | What the server calls itself. Required. | — |
instructions | What a model is told before it calls anything. | none |
anonymous | Whether the endpoint answers without a credential. | false |
origins | Browser origins the endpoint answers. | none |
hosts | A host of its own per authority. | none |
manifest | URL of the web manifest whose icons the server shows. | none |
In a manifest:
| Directive | Value | Meaning |
|---|---|---|
mcp:tool | boolean | Publish, or withdraw, the methods at and under this node. |