Runtype
ExploreExplainer

MCP or a plain API: which one should you expose to agents

MCP and a REST API answer different questions. How they differ on discovery, schemas, auth, state and streaming, and when to expose one, the other or both.

Last updated 6 min read

A REST API defines what your system can do. MCP defines how an agent finds out at runtime which of those things exist and how to call them. If the caller is your own code, keep the API. If the caller is a model running inside a client you do not control, put MCP in front of the same handlers.

The situation: you already have an API and someone wants an MCP server

The API has been in production for years. It has an OpenAPI document, a key-issuing flow, rate limits per customer, and an agent can already reach it through a generic HTTP tool. Then a customer asks for an MCP server, or an engineer wires one up over a weekend, and the question is whether you have just signed up to maintain two integration surfaces forever.

Drift is what makes the second surface expensive. An endpoint gains a required field, the tool schema published over MCP still advertises the old shape, and the failure surfaces inside somebody else's chat client where you have no traces and no way to tell whose account it happened in.

What each one actually defines

DimensionREST APIMCP server
DiscoveryOut of band. A human reads docs and writes the client.At runtime. The client calls tools/list and gets the current tool set.
SchemaAn OpenAPI document you publish, written for developers.JSON Schema per tool, returned inline with a description written for a model.
AuthWhatever you chose: bearer key, OAuth, mTLS, signed request.The HTTP transport standardises on OAuth with protected-resource metadata; local stdio servers inherit the host process environment.
StatefulnessUsually stateless per request.Stateless per request since revision 2026-07-28: each request carries the protocol version and capabilities in _meta, with no handshake and no session id. Earlier revisions opened with initialize and could hold a session.
StreamingAd hoc: SSE, chunked responses, websockets, polling.Progress notifications and partial results ride the same call.
Who the caller isYour code, written once against docs.A model choosing, this turn, from a list it just read.

The last row drives every other one. A developer integrating against your API can read a 30-page guide, notice that status accepts four values, and handle the 409 by asking a colleague. A model gets one description string, one JSON Schema, and whatever your error body says, and it has to decide within a single turn.

Discovery is the difference that changes your design

An MCP client asks the server what it can do, and asks again when the server sends a list-changed notification. That means you can add, rename or retire a capability without anybody shipping a client release, and it means the tool set can differ per connection: a credential scoped to read-only gets a shorter list than an admin credential.

Nothing about that is impossible over plain HTTP. You could publish a machine-readable manifest, keep it current, and write a loader that fetches it before each run. The protocol exists so that Claude Desktop, Cursor, an internal copilot and a customer's own agent all do that same thing the same way, without you writing five loaders. If you want the longer version of how the protocol is put together, what an MCP server is covers the primitives.

Errors, versioning and the context bill

Three practical differences show up after the first week, and none of them appear in an architecture diagram.

Error semantics. A failed MCP tool call generally comes back as a normal result carrying an error flag and readable text, rather than as a protocol-level error, so the model can read the message and correct its arguments. Your 422 body was written for a developer with a schema open in another tab. Rewrite those messages to say what to do next: amount_cents must be at most the remaining balance of 4200, not validation_failed.

Versioning. REST gave you /v1 and /v2 living side by side while clients migrated. MCP has no equivalent convention for a tool. Changing an argument changes it for every connected client on their next list refresh, so a breaking change means publishing a new tool name and leaving the old one in place until you can see nobody calling it.

Context cost. Every tool definition is sent to the model on every turn of every conversation. Forty tools at 150 tokens of name, description and schema is 6,000 tokens before the user has typed anything, paid on each turn. That is the mechanism behind the advice in how many tools an agent should have, and it is why a faithful one-to-one mirror of a large REST surface is usually the wrong first MCP server.

Should I build an MCP server or just use my REST API

Decide by naming the caller, not by comparing the technologies.

  1. The caller is your own backend code. Use the API. Deterministic code does not need runtime discovery, and a JSON-RPC layer buys you nothing over a typed client.
  2. The caller is a model inside an agent you build and deploy. Use the API, wrapped as tool definitions you generate from your OpenAPI document at build time. You control both ends, so you already know the tool set.
  3. The caller is a model in a client you do not control: a customer's IDE, a desktop assistant, an agent another team wrote. Build the MCP server. This is the case discovery was designed for.
  4. The caller is a customer's own agent, acting for one of their end users. Build the MCP server, and treat the connection credential as the only source of tenant identity.
  5. You have both audiences. Expose both, from one set of handlers. The API stays the contract for integrations; the MCP server is a curated projection of it for models.

Case five is the common one in a product with customers, and it is only expensive if you build the MCP server as a second codebase. Built as a projection, the shared piece is the handler and its authorization; the MCP layer holds tool names, descriptions, argument schemas and the allowlist of what is exposed. Exposing an agent as an API covers the same shape from the other direction.

Generating an MCP layer over an existing API

Generators that read an OpenAPI document and emit one tool per operation are a reasonable starting point and a poor finishing point. They inherit summaries written for humans, they emit every path parameter as a model-fillable argument, and they turn a 60-endpoint API into a 60-tool server.

Three edits turn generated output into something a model can use. Collapse related endpoints into task-shaped tools, so that find the customer, list their payments, refund the matching one is one tool rather than three calls the model has to sequence. Rewrite each description to say when to call the tool and when not to. Remove every argument the caller should not choose, tenant identifiers first.

{
  "name": "refund_payment",
  "description": "Refund a settled payment for the connected account. Use only when the customer has asked for a refund. Does not cancel a subscription.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "payment_id": {
        "type": "string",
        "description": "Payment id from list_payments, format pay_XXXX"
      },
      "amount_cents": {
        "type": "integer",
        "minimum": 1,
        "description": "Omit for a full refund"
      }
    },
    "required": ["payment_id"],
    "additionalProperties": false
  }
}

There is no account_id in that schema, on purpose. The account comes from the credential the client authenticated with. A model will confidently fill any field you declare, including an identifier it read out of a support ticket pasted into the conversation, which is how a multi-tenant MCP server turns into a cross-tenant read.

Endpoints that should never become tools

  • Anything whose authorization currently depends on the caller passing the right scope value. Move tenant resolution to the credential before exposing it.
  • Destructive operations with no natural bound: bulk delete, account closure, anything that cannot be reversed by another tool in the same set.
  • Admin and internal endpoints that exist because your own staff are trusted. Trust does not transfer to a model reading attacker-controlled text.
  • Reads that return unbounded pages. A model that asks for every record will get every record, then blow the context window on the response.
  • Anything whose response body carries other people's PII or raw customer text you have not redacted, since that content becomes instructions the model has read.

Run this list against your own routes before generating anything. It is faster than reviewing 60 generated tools, and it is the part that generators cannot do for you.

Where this gets easier

Runtype registers an existing HTTP API as agent tools and publishes a curated tool set over an MCP surface with OAuth, API-key or public authentication, so the same handlers answer both front doors instead of becoming two integrations that drift. Tenancy is a property of the resource rather than an argument: each one is marked internal, tenant-isolated or end-user-isolated with an assurance floor, and a request whose identity scope does not meet it is rejected before execution. Secrets stay server-side as {{secret:NAME}} references the model never sees, tool search activates automatically past about 20 tools, and per-tool approval gates let a destructive call pause for a human before it runs. More on the surface itself is on the MCP server page.

Frequently asked questions

Do I need an MCP server if my agent can already call my REST API?
No, if the only caller is an agent you build and deploy yourself. You control the tool definitions in that case, so you can generate them from your OpenAPI document and skip the protocol entirely. MCP earns its place when the calling client belongs to someone else, because then discovery has to happen at runtime rather than at your build time.
Does an MCP server replace my API?
It sits in front of it. A well-built MCP server is a projection over the same handlers, with descriptions and argument schemas rewritten for a model and the dangerous endpoints left out. The API stays the system of record for every non-agent caller you already have.
How do I stop an MCP tool from reading another tenant's data?
Resolve the tenant from the credential on the connection, never from a tool argument. A model will fill any field you declare, including a tenant id it inferred from conversation text. If a tool needs a tenant identifier in its input schema at all, treat that as a design error rather than a validation problem.
Can one MCP server expose too many tools?
Yes. Every tool definition is sent to the model on every turn, so a one-to-one mapping of a large REST surface both costs tokens and lowers selection accuracy. Fewer, task-shaped tools generally beat a faithful mirror of your endpoint list.