Room 42, home

Wrap your own API as an MCP tool

Expose one endpoint of your own API to Claude as a single MCP tool, with a clear input schema and a quick CLI test loop.

MCPadvanced8 min readUpdated 13 Jun 2026

In short

  1. 01Wrapping an API as an MCP tool lets Claude call one of your endpoints on its own instead of you pasting data back and forth.
  2. 02Start with a single read-only endpoint and a tight tool definition: a clear name, a precise description, and a typed input schema.
  3. 03Scaffold the server with the official plugin, then register it using claude mcp add and pick a local, project, or user scope.
  4. 04Test the loop with claude mcp list and a real prompt, and when Claude misuses the tool, sharpen the description or schema.
Contents · 12 sections
  1. 1Start with one endpoint, not your whole API
  2. 2What is an MCP tool?
  3. 3How does an MCP tool call flow?
  4. 4How do I write a strong input schema?
  5. 5Scaffold the server with Claude
  6. 6Register the server with the CLI
  7. 7Where should the MCP server config live?
  8. 8How do I test that the tool works?
  9. 9When Claude misuses the tool, fix the description
  10. 10Return results Claude can read
  11. 11Keep large results from flooding context
  12. 12A safe path to the first working tool

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 toolA function the model can call to do something: query a database, search records, fetch a status. The model decides when to call it. 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, like get_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 SchemaA standard, language-neutral way to describe the shape of JSON data: which fields exist, their types, and which are required. 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:

  1. Type every property. Use string, number, integer, boolean, array, or object. This stops Claude from passing a number where you expect a string.
  2. Describe every property. The description on a property tells Claude what the value means and the format you expect, like "ISO 8601 date, like 2026-06-13".
  3. List what is required. The required array 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-official

If Claude Code reports that the marketplace is not found, add it first, then retry the install:

/plugin marketplace add anthropics/claude-plugins-official

Once 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-server

Give Claude the specifics it needs to generate a correct wrapper:

Try this prompt
Build a local stdio MCP server that exposes one tool named get_order. It wraps GET https://api.example.com/orders/{order_id} on my service. The tool takes one required string argument order_id and returns the JSON response as text. Read the API base URL from an environment variable API_BASE_URL and the key from API_KEY. Do not add any other tools yet.

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 stdioStandard input/output: the plain text streams a program reads from and writes to. The host launches the server and talks to it over those streams, with no network. 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.js

For 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:

ScopeLoads inShared with teamStored in
localCurrent project onlyNo~/.claude.json
projectCurrent project onlyYes, via version control.mcp.json in project root
userAll your projectsNo~/.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/mcp

A 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 list

Get the details for one server to confirm the transport and config are right:

claude mcp get orders

Inside 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:

Try this prompt
What is the status of order ORD-4821?

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 description is ambiguous. State plainly what the tool does and when to use it.
  • Missing argument · add the property to the required array.
  • Invalid value · add a type, an enum, or a format note in the property description.

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
claude

A 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:

  1. Pick one read-only endpoint.
  2. Write the name, description, and inputSchema by hand so the contract is exactly what you mean.
  3. Scaffold the server with the mcp-server-dev plugin, feeding it that contract.
  4. Register it at local scope with claude mcp add so the experiment stays private.
  5. Confirm it connects with claude mcp list and /mcp.
  6. 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.