An MCP server over an existing API is an MCP server that gives a language model access to a service through the service's existing REST, GraphQL or RPC interface. The server does not copy the API one to one. It selects the operations worth exposing, turns them into tools that a model can choose and fill in correctly, and shapes the results to fit the model's context.
The MCP specification does not define how such a server should be designed. It defines the format of tools, the separation of errors, caching hints and security requirements; the rest is design guidance. This article follows the specification version 2026-07-28 and the guide "Writing effective tools for agents", published by Anthropic in September 2025. Basic concepts of the protocol are described in the article What is MCP, and the fields of a tool definition in the article Tools in MCP.
Overview
| Property | Value |
|---|---|
| Object | MCP server that wraps the API of an existing service |
| Main primitive | Tools; resources and prompts are optional |
| Requirements | Specification 2026-07-28: tools, caching, authorization, Security Best Practices |
| Design guidance | "Writing effective tools for agents", Anthropic, September 2025 |
| Key decisions | Set of tools, descriptions, parameters, response size, errors, authorization, state |
| Specification version | 2026-07-28 |
Why an API is not exposed as is
An API is designed for programs. A developer reads the documentation once and writes code that calls endpoints in a fixed order, stores identifiers, walks through every page of a list and maps numeric codes to meanings.
A model chooses a tool in every conversation, from the names, descriptions and input schemas in its context. Every tool definition and every result takes space in that context. A long list of similar tools makes the choice harder, a page of raw JSON consumes tokens, and an opaque identifier is easy to confuse with another one.
The Anthropic guide names tools that merely wrap existing endpoints as a common mistake and recommends a few tools aimed at specific high-impact workflows. The MCP server becomes an adaptation layer between the model and the unchanged API.
History
The table lists the changes of the specification that matter for the design of tools.
| Date | Event |
|---|---|
| 26 March 2025 | Specification 2025-03-26: tool annotations that describe the behavior of a tool |
| 18 June 2025 | Specification 2025-06-18: structured tool output (outputSchema, structuredContent), resource links in results, the title field |
| 11 September 2025 | Anthropic publishes the guide "Writing effective tools for agents" |
| 25 November 2025 | Specification 2025-11-25: input validation errors are returned as tool execution errors (SEP-1303) |
| 28 July 2026 | Specification 2026-07-28: stateless protocol, explicit state handles, caching hints ttlMs and cacheScope, deterministic order of tools |
Choosing the operations
Workflows instead of endpoints
A tool covers a task that a user asks for, not a single HTTP call. In the examples of the Anthropic guide, one schedule_event tool finds a free slot and creates the event instead of separate tools for listing users, listing events and creating an event; a search_logs tool returns only relevant lines instead of read_logs.
A tool that combines several API calls moves the logic from the model to the server: the server performs the calls in the right order and joins the data. The guide also warns that too many tools, or tools with overlapping purposes, distract the model from efficient strategies. It prefers a search with filters to a full list, which makes the model read records that do not matter for the task.
Read and write operations
Operations that read data and operations that change it carry different risks. A server can expose read operations broadly and only those write operations that the target workflows need. A rarely needed, irreversible or administrative operation can stay outside the server: the user performs it in the service's own interface.
The final control stays with the host: the specification recommends that a human always be able to deny a tool call.
Annotations for destructive operations
A tool definition can carry annotations, optional properties that describe its behavior. The schema of version 2026-07-28 defines five of them:
| Annotation | Meaning | Default |
|---|---|---|
title |
Human-readable name of the tool | none |
readOnlyHint |
The tool does not modify its environment | false |
destructiveHint |
The tool may perform destructive updates; false means only additive updates |
true |
idempotentHint |
A repeated call with the same arguments has no additional effect | false |
openWorldHint |
The tool interacts with an open world of external entities, such as the web | true |
destructiveHint and idempotentHint are meaningful only when readOnlyHint is false. The defaults describe the most cautious case, so a server sets readOnlyHint: true on read-only tools and destructiveHint: false on tools that only add data.
Annotations are hints. Clients must treat them as untrusted unless the server is trusted, and an annotation does not replace access control on the server.
Descriptions and names
Tool names
Two connected servers can expose tools with the same name, such as search, and the specification lets clients prefix tool names with a server identifier. The Anthropic guide recommends namespacing on the server side as well: by service (asana_search, jira_search) and by resource (asana_projects_search, asana_users_search). According to the guide, the choice between prefixes and suffixes had a noticeable effect in its evaluations and differs between models.
Tool descriptions
The description is the main source from which the model learns when to use a tool; the schema calls it a hint to the model. The Anthropic guide recommends describing a tool the way one would explain it to a new member of the team. What an experienced user of the API knows implicitly is written out: query formats, domain terms, relationships between entities, limits. According to the guide, even small refinements of descriptions can lead to large improvements in how a model uses the tools.
Information that concerns the whole server, rather than one tool, can go into the instructions field of the server/discover result. The schema describes it as natural-language guidance that helps the model use the server and that should not duplicate tool descriptions.
Parameter names
Parameter names are part of the description: according to the guide, a parameter named user_id is clearer than user. Each parameter has its own description with the expected format and an example. A schema with additionalProperties: false rejects unknown arguments.
Parameters
Enumerations and default values
An API often accepts codes and flags that only make sense with documentation. In a tool they become explicit choices: a status that the API encodes as a number becomes an enum of readable values, and the server maps them to the codes. The model chooses from a closed list instead of guessing.
Parameters that most calls do not need get default values. The JSON Schema default keyword states the value in the schema, and the server applies it when the argument is missing.
Readable identifiers
APIs identify objects with internal identifiers, often UUIDs. According to the Anthropic guide, resolving UUIDs into meaningful names, or even into a simple numbering, significantly improves the precision of the model in retrieval tasks and reduces hallucinations.
A server over an API therefore accepts the identifiers people use: a project key, an issue key such as WEB-142, a user name, the word "me" for the current user. The server resolves them into internal identifiers itself.
Response size
Filtering and limits
A tool that can return a large amount of data limits it with pagination, range selection, filtering or truncation and sensible defaults. For Claude Code, the guide states, tool responses are limited to 25,000 tokens by default.
The protocol defines cursor pagination only for list operations such as tools/list, not for the content of a tools/call result. A search tool therefore has its own parameters, such as a limit and a cursor that the result returns for the next call.
Truncation with guidance
When a result is cut, the tool says so and explains how to get the rest: narrow the filter, request the next page, search for a specific item. The guide recommends such text to steer the model toward small targeted searches instead of one broad one.
Concise and detailed formats
The guide describes a response_format parameter with the values "concise" and "detailed". The concise format returns only what the model needs to answer, for example the text of messages. The detailed format adds identifiers and fields that are needed for follow-up calls. In the guide's example, the concise response took about a third of the tokens of the detailed one.
The fields of the response are chosen for the model, not copied from the API: the guide prefers name and file_type to uuid and mime_type.
Errors the model can act on
The specification separates protocol errors, such as an unknown tool, from tool execution errors, such as an API failure, a business logic error or invalid arguments. The latter are returned in the tool result with isError: true, and the specification describes them as actionable feedback that the model can use to correct the call.
A server over an API therefore translates the errors of the API. An HTTP status with an internal code tells the model little; a text that names the wrong value and the valid ones lets it recover. The guide recommends specific, actionable messages instead of opaque error codes and stack traces.
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"resultType": "complete",
"content": [
{
"type": "text",
"text": "Project \"WEBB\" not found. Available project keys: WEB, API, MOBILE. Call tracker_search_issues again with one of these keys in the project parameter."
}
],
"isError": true
}
}Authorization and user identity
How an MCP server over HTTP obtains and validates tokens is described in the article Authorization in MCP. For a server over an API, two rules of the specification matter most.
First, the server accepts only tokens issued for itself and validates every incoming token. The identity of the user is taken from the verified token, not from a tool argument. The list of tools may depend on the authorization of the request, for example on the granted scopes.
Second, the server must not pass the token it received from the client on to the API (token passthrough). When the server calls the upstream API, it may act as an OAuth client of that API and uses a separate token issued by the API's own authorization server. The specification does not prescribe how the server obtains that token or how it links the user to an account in the API.
A server that connects clients to a third-party API with one static client identifier is called an MCP proxy server in Security Best Practices. Such a server is exposed to the confused deputy problem, and the document requires it to keep per-client consent and to check it before the third-party authorization flow starts. The document also recommends a minimal initial set of scopes, with additional scopes requested when a privileged operation is first attempted. Security risks are described in the article Security of MCP. A local server over stdio takes credentials, such as an API key, from its environment.
State between calls
Since 2026-07-28 the protocol has no session; the changes are described in the article Stateless MCP. A server that needs state between calls, such as a draft or an open transaction, returns an explicit handle from the tool that creates the state and accepts it as an argument of later calls. The model carries the handle forward.
The specification gives non-normative guidance for handles:
- Authorization. On an authenticated server a handle is a name, not a permission: the server checks the caller's rights to the handle on every call.
- Opacity. A handle does not encode internal structure that invites parsing or guessing.
- Lifetime. The retention period is stated in the description of the creating tool, for example "drafts expire after 24 hours of inactivity", so that the model sees it.
- Expiry. A call with an expired or unknown handle returns a tool execution error that says so, and the model can create a new handle.
Security Best Practices adds that possession of a handle is not authentication. Handles come from a secure random generator and are bound on the server to the user from the verified token, for example by storing the state under the key <user_id>:<handle>.
Structured output
A tool can declare outputSchema, the JSON Schema of its result. The structured result is returned in structuredContent and must conform to the schema; for compatibility the tool should also return the serialized JSON in a text block. The fields themselves are described in the article Tools in MCP.
For a server over an API, the output schema is a separate contract, not a copy of the API response: it contains the fields that the tool returns after filtering and renaming.
Caching hints
Since 2026-07-28 the results of server/discover, list methods and resources/read carry caching hints: ttlMs, the time in milliseconds during which the result stays fresh, and cacheScope. The results of tools/call are not on this list.
cacheScope: "public" means that the result contains no user-specific data and any shared cache may serve it to any user. "private" allows reuse only within the same authorization context, so a list of tools that depends on the scopes of the token is "private". cacheScope does not replace access control. Tools are returned in a deterministic order, which, according to the specification, also improves the hit rate of the model's prompt cache.
Example with an issue tracker API
The example uses a fictional API of an issue tracker at api.tracker.example. It is not a real service. The API has four endpoints:
| Endpoint | What it does |
|---|---|
GET /v2/projects/{project_id}/issues |
List of issues; filters state (code 1, 2 or 3) and assignee_id (UUID); page, per_page; every issue with all its fields |
POST /v2/projects/{project_id}/issues |
Creates an issue |
POST /v2/issues/{issue_id}/transitions |
Changes the status by the identifier of a transition |
DELETE /v2/issues/{issue_id} |
Deletes an issue |
Before the redesign
A direct mirror produces one tool per endpoint. The names repeat the paths, the descriptions repeat the HTTP method, and the parameters require UUIDs and numeric codes that the model can only guess.
[
{
"name": "get_v2_projects_project_id_issues",
"description": "GET /v2/projects/{project_id}/issues",
"inputSchema": {
"type": "object",
"properties": {
"project_id": { "type": "string" },
"state": { "type": "integer" },
"assignee_id": { "type": "string" },
"page": { "type": "integer" },
"per_page": { "type": "integer" }
},
"required": ["project_id"]
}
},
{
"name": "post_v2_issues_issue_id_transitions",
"description": "POST /v2/issues/{issue_id}/transitions",
"inputSchema": {
"type": "object",
"properties": {
"issue_id": { "type": "string" },
"transition_id": { "type": "string" }
},
"required": ["issue_id", "transition_id"]
}
},
{
"name": "delete_v2_issues_issue_id",
"description": "DELETE /v2/issues/{issue_id}",
"inputSchema": {
"type": "object",
"properties": {
"issue_id": { "type": "string" }
},
"required": ["issue_id"]
}
}
]After the redesign
The redesigned server has three tools with the tracker_ prefix. They accept project and issue keys, readable statuses and user names; the server resolves them into identifiers, codes and transitions and walks through the pages of the API itself. Deletion is not exposed and stays in the tracker's own interface.
[
{
"name": "tracker_search_issues",
"title": "Search issues",
"description": "Search issues in one project of the tracker. Returns at most `limit` issues, newest first. Use the concise format to answer questions and the detailed format when you need issue fields for a follow-up call. If more issues match, the result says so; narrow the query instead of raising the limit.",
"inputSchema": {
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "Project key, for example WEB"
},
"status": {
"type": "string",
"enum": ["open", "in_progress", "done"],
"description": "Filter by status. Omit to search all statuses"
},
"assignee": {
"type": "string",
"description": "Username of the assignee, or me for the current user"
},
"query": {
"type": "string",
"description": "Words to find in the title or description"
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 50,
"default": 20
},
"response_format": {
"type": "string",
"enum": ["concise", "detailed"],
"default": "concise"
}
},
"required": ["project"],
"additionalProperties": false
},
"outputSchema": {
"type": "object",
"properties": {
"issues": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": { "type": "string" },
"title": { "type": "string" },
"status": { "type": "string" },
"assignee": { "type": "string" }
},
"required": ["key", "title", "status"]
}
},
"truncated": { "type": "boolean" }
},
"required": ["issues", "truncated"]
},
"annotations": {
"readOnlyHint": true,
"openWorldHint": false
}
},
{
"name": "tracker_create_issue",
"title": "Create issue",
"description": "Create an issue in a project. Returns the key of the new issue, for example WEB-143. Each call creates a new issue, so do not repeat a call that succeeded.",
"inputSchema": {
"type": "object",
"properties": {
"project": { "type": "string", "description": "Project key, for example WEB" },
"title": { "type": "string", "maxLength": 200 },
"description": { "type": "string" },
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
"default": "medium"
}
},
"required": ["project", "title"],
"additionalProperties": false
},
"annotations": {
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": false
}
},
{
"name": "tracker_change_issue_status",
"title": "Change issue status",
"description": "Move an issue to another status. The previous status is replaced. Moving an issue to the status it already has changes nothing.",
"inputSchema": {
"type": "object",
"properties": {
"issue": { "type": "string", "description": "Issue key, for example WEB-142" },
"status": { "type": "string", "enum": ["open", "in_progress", "done"] }
},
"required": ["issue", "status"],
"additionalProperties": false
},
"annotations": {
"readOnlyHint": false,
"destructiveHint": true,
"idempotentHint": true,
"openWorldHint": false
}
}
]The annotations follow the definitions of the schema. Creation only adds data and is not idempotent, because each call creates a new issue. The status change replaces the previous value, which is not an additive update, so destructiveHint stays true; a repeated call with the same status changes nothing, so the tool is idempotent. The tracker is a closed domain, so openWorldHint is false.
REST API endpoint and MCP tool
The table compares a typical endpoint of an existing API with a tool of a server designed over it.
| REST API endpoint | MCP tool over the API | |
|---|---|---|
| Consumer | Code written by a developer | A model choosing in each conversation |
| Unit | One resource and one HTTP method | One task, possibly several API calls |
| Interface description | Documentation, OpenAPI | Name, description, inputSchema, read by the model |
| Identifiers | Internal, often UUIDs | Readable keys and names; resolved by the server |
| Volume of results | Full objects, pages chosen by the client | Selected fields, limits, concise and detailed formats |
| Errors | HTTP status and error code | isError: true with a text that explains the fix |
| Side effects | Described in documentation | Annotations readOnlyHint, destructiveHint, idempotentHint |
| Credentials | Token of the API | Token issued for the MCP server; separate token for the API |
| State | Resources and identifiers of the API | Explicit handles passed as arguments |
The endpoint remains the basis: the logic of the service, its validation and its access control stay in the API. The tool adds the layer that the model needs: a choice of operations, wording, simplification and limits.
The two interfaces evolve separately: a new tool description can change the behavior of the model without any change in the API, so tools are tested with models, as described in the article Testing MCP servers. General methods of evaluating applications based on language models are described in the article Testing LLM applications.
Terms
| Term | Meaning |
|---|---|
| MCP server over an existing API | MCP server that gives a model access to a service through its existing API and adapts its operations into tools |
| Tool annotations | Optional hints in a tool definition about its behavior: readOnlyHint, destructiveHint, idempotentHint, openWorldHint |
| Tool namespacing | Common prefix or suffix in tool names that groups them by service or resource |
| Response format | Tool parameter that lets the model choose a concise or a detailed result |
| Tool execution error | Tool result with isError: true and a text that the model can use to correct the call |
| Token passthrough | Forwarding the client's token to another service; forbidden by the specification |
| MCP proxy server | MCP server that connects clients to a third-party API and acts as a single OAuth client of that API |
| State handle | Identifier that a tool returns and the model passes back in later calls |
Sources
- Model Context Protocol specification, version 2026-07-28(opens in a new tab)
- Tools, specification 2026-07-28(opens in a new tab)
- Caching, specification 2026-07-28(opens in a new tab)
- Pagination, specification 2026-07-28(opens in a new tab)
- Authorization: Security Considerations, specification 2026-07-28(opens in a new tab)
- Key Changes, specification 2026-07-28(opens in a new tab)
- Schema of the specification 2026-07-28 (schema.ts), GitHub(opens in a new tab)
- Security Best Practices(opens in a new tab)
- Tool Annotations as Risk Vocabulary: What Hints Can and Can't Do, MCP blog, 16 March 2026(opens in a new tab)
- Writing effective tools for agents, Anthropic, 11 September 2025(opens in a new tab)
- JSON Schema Validation, draft 2020-12(opens in a new tab)