To give Claude direct access to one of your own API endpoints, you build a small MCP server that exposes that endpoint as a single tool, define the tool's input schema in JSON, and register the server with the Claude Code CLI. Once it is connected, Claude calls your endpoint on its own when a task needs it, instead of you pasting data back and forth.
This guide shows the shape of that tool, how to register it, and how to test the loop. If you are new to the protocol, read MCP fundamentals first for the mental model of hosts, clients, servers, and primitives.
Start with one endpoint, not your whole API
The instinct is to expose every route. Resist it. A single, well-described tool that maps to one endpoint is easier for Claude to use correctly and easier for you to reason about.
Pick one endpoint with a clear job. Good first candidates:
- A read that returns one record by id, like
GET /orders/{id}. - A search that takes a few filters, like
GET /tickets?status=open. - A status check, like
GET /deploys/latest.
Start with a read-only endpoint. A tool that only reads data cannot damage
anything when Claude calls it while you are still learning the loop. You add
write tools later, once the read path works.Common gotchaClaude decides on its own when to fire a tool, so a wrapped write endpoint (delete, refund, send) can run a destructive action without you confirming it.When you do add one, make the API idempotent or require an explicit confirmation argument, never just point the tool at a raw DELETE.
What is an MCP tool?
An MCP tool
is a named function the model can call. The server advertises it in a tools/list
response, and each tool definition carries four things that matter to you:
name· a unique identifier the model uses to call the tool, likeget_order.title· an optional human-readable label for display.description· plain text that tells the model what the tool does and when to use it. This is the single most important field for getting good calls.inputSchema· a JSON Schema object that defines the arguments the tool accepts.
Here is the shape of one tool definition, taken from the protocol's own example format. This wraps a read endpoint that fetches one order by id:
{
"name": "get_order",
"title": "Order Lookup",
"description": "Fetch one order by its id. Returns status, line items, and total. Use when the user asks about a specific order.",
"inputSchema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The order id, like ORD-4821"
}
},
"required": ["order_id"]
}
}The model reads description to decide whether the tool fits the task, then
reads inputSchema to build valid arguments. Both are part of your contract with
the model, so write them as carefully as you would write a function signature.
For the mechanics of how a model decides to call a tool, see tool calling explained.
How does an MCP tool call flow?
When Claude decides your tool fits a task, the host routes the call to your server, your server hits your API, and the result flows back as context.
Your server is the thin layer in the middle. It receives the tool call, maps the arguments onto an HTTP request to your API, and returns the response as a tool result. You do not write any code that calls the model. Claude is the caller.
How do I write a strong input schema?
The input schema is where most tool quality comes from. A vague schema produces vague calls. Treat each property like a typed parameter with documentation.
Three rules cover the common cases:
- Type every property. Use
string,number,integer,boolean,array, orobject. This stops Claude from passing a number where you expect a string. - Describe every property. The
descriptionon a property tells Claude what the value means and the format you expect, like"ISO 8601 date, like 2026-06-13". - List what is required. The
requiredarray names the properties the call cannot omit. Leave optional filters out of it.
Here is a richer schema for a search endpoint that takes a status filter and a result limit:
{
"name": "search_tickets",
"title": "Ticket Search",
"description": "Search support tickets by status. Returns a list of matching tickets, newest first.",
"inputSchema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["open", "pending", "closed"],
"description": "Ticket status to filter by"
},
"limit": {
"type": "integer",
"description": "Maximum number of tickets to return. Defaults to 20 on the server."
}
},
"required": ["status"]
}
}The enum constraint matters. It tells Claude the only valid values for
status, so it will not invent a fourth state your API rejects. Use enum
whenever an argument has a fixed set of options.
Scaffold the server with Claude
You do not have to write the server boilerplate by hand. Anthropic ships an official plugin that scaffolds an MCP server for you. (To understand the pieces it generates, the build an MCP server guide builds one from scratch.) Inside a Claude Code session, install it:
/plugin install mcp-server-dev@claude-plugins-officialIf Claude Code reports that the marketplace is not found, add it first, then retry the install:
/plugin marketplace add anthropics/claude-plugins-officialOnce it is installed, run the build skill. Claude asks about your use case and scaffolds a local stdio server or a remote HTTP server:
/mcp-server-dev:build-mcp-serverGive Claude the specifics it needs to generate a correct wrapper:
The more precise you are about the endpoint, the arguments, and the auth, the closer the generated server is to working on the first run.
Register the server with the CLI
A server is useless until a host connects to it. You register it with
claude mcp add. The transport you choose depends on where the server runs.
For a local server that runs as a stdio
subprocess on your machine, pass the command after a -- separator. Everything
after -- is handed to the server untouched, so Claude does not try to parse the
server's own flags:
claude mcp add --transport stdio --env API_KEY=YOUR_KEY orders \
-- node ./servers/orders-server.jsFor a server you host behind a URL, use the HTTP transport. This is the recommended transport for remote servers, and it supports tokens and OAuth:
claude mcp add --transport http orders https://mcp.example.com/mcp \
--header "Authorization: Bearer YOUR_TOKEN"The --env flag accepts multiple KEY=value pairs and passes them into the
server's environment. Keep at least one other option between --env and the
server name, since the CLI reads the next token after --env as another pair.
Where should the MCP server config live?
The --scope flag controls where Claude Code stores the server entry and who can
see it. There are three scopes:
| Scope | Loads in | Shared with team | Stored in |
|---|---|---|---|
local | Current project only | No | ~/.claude.json |
project | Current project only | Yes, via version control | .mcp.json in project root |
user | All your projects | No | ~/.claude.json |
local is the default and keeps the server private to you in one project. Use
project when you want the whole team to get the same tool, since the
.mcp.json file is meant to be checked into version control:
claude mcp add --transport http --scope project orders https://mcp.example.com/mcpA project-scoped .mcp.json follows a standard shape. For a remote endpoint with
auth pulled from an environment variable, it looks like this:
{
"mcpServers": {
"orders": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"headers": {
"Authorization": "Bearer ${API_TOKEN}"
}
}
}
}Claude Code expands ${API_TOKEN} from your environment, so the secret stays out
of the file. You can also write ${API_TOKEN:-default} to supply a fallback when
the variable is unset.Common gotchaBecause .mcp.json is meant to be committed, pasting a raw key straight into its headers (or running claude mcp add --scope project with a literal Bearer YOUR_TOKEN) ships your credential to every clone and into git history forever.Always reference the secret as ${API_TOKEN} and keep the real value in your environment, so the project file holds only the variable name.
How do I test that the tool works?
Registration is only half the job. Confirm the server connects and the tool shows up.
List your servers and check the status:
claude mcp listGet the details for one server to confirm the transport and config are right:
claude mcp get ordersInside an interactive session, the /mcp panel shows the tool count next to each
connected server and flags a server that advertises tools but exposes none. A
project-scoped server from .mcp.json shows as pending approval until you
approve it in an interactive run.
Then drive a real call with a prompt that needs the tool. Watch which tool Claude picks and what arguments it builds:
If Claude calls get_order with {"order_id": "ORD-4821"} and you get the
record back, the loop works end to end.
When Claude misuses the tool, fix the description
If Claude calls the wrong tool, skips a required argument, or passes a bad value, the problem is almost always the description or the schema, not the model.
Tighten the contract and reconnect:
- Wrong tool chosen · the
descriptionis ambiguous. State plainly what the tool does and when to use it. - Missing argument · add the property to the
requiredarray. - Invalid value · add a
type, anenum, or a format note in the propertydescription.
A short, specific description beats a long, vague one. Claude Code truncates a tool description at 2KB, so put the critical detail near the start.
Return results Claude can read
Your server returns tool results as content. The common case is a text block that carries the JSON your API returned:
{
"content": [
{ "type": "text", "text": "{\"order_id\":\"ORD-4821\",\"status\":\"shipped\"}" }
],
"isError": false
}When your API call fails, report it as a tool execution error so Claude can react
instead of treating the failure as data. Set isError to true and put a plain
message in the text:
{
"content": [
{ "type": "text", "text": "Order lookup failed: 404 Not Found for ORD-9999" }
],
"isError": true
}With isError set, Claude sees the call failed and can retry with a different
argument or tell the user what went wrong, rather than passing a 404 body forward
as if it were a real order.
Keep large results from flooding context
If your endpoint can return a large payload, like a full schema or a long list, mind the output limits. Claude Code warns when a tool's output exceeds 10,000 tokens and caps output at 25,000 tokens by default. Raise that ceiling for a session when you need it:
export MAX_MCP_OUTPUT_TOKENS=50000
claudeA better fix is to make the tool return less. Add a limit argument to the
schema, paginate, or return only the fields the task needs. A focused result is
cheaper and easier for Claude to use than a giant blob.
A safe path to the first working tool
The shortest route to a tool you trust:
- Pick one read-only endpoint.
- Write the
name,description, andinputSchemaby hand so the contract is exactly what you mean. - Scaffold the server with the
mcp-server-devplugin, feeding it that contract. - Register it at
localscope withclaude mcp addso the experiment stays private. - Confirm it connects with
claude mcp listand/mcp. - Drive a real prompt and check the tool name and arguments Claude builds.
Once the read path is solid and the input schema produces clean calls, you can
add a second tool, widen the scope to project for your team, or wrap a write
endpoint with the same discipline. The MCP configuration guide
covers scopes and transports in more depth.