๐บ Prefer to watch? 90-second YouTube Short ยท ๐ฌ Telegram
Originally published on software-engineer-blog.com.
Same server. Same database. Same functions.
Your service already has an API. It has had one for years. It is documented, it is authenticated, it is in production, and it works.
So why is everyone suddenly standing up a /mcp endpoint right next to it?
Not because the old one is broken. Behind both doors is the same server, the same database, the same Python functions. Nothing underneath changes. The difference is one sentence long:
The API is written for a developer. The MCP endpoint is written for a model.
Everything else in this article is a consequence of that sentence โ including the one part of the job that no library will do for you.
Why a plain endpoint is not enough
Think about how your API actually got called.
Somewhere, a human being opened your documentation. They read that the path is /v1/invoices, that it takes a customer_id and a from and a to, that the dates are ISO-8601 and that the response is paginated. Then they went back to their editor and hard-coded that knowledge into a client.
All of the understanding happened outside the wire. The endpoint itself never explained anything. Ask it what it is for and it has no answer โ there is no request you can send it that means "describe yourself." It was never designed to have one, because the reader was always going to be a person with a browser.
A model does not have a browser, and it was not in the room when your docs were read. If the only thing on the wire is POST /v1/invoices, the model is guessing.
What MCP changes is the first move
Here is the actual shift, and it is smaller than it sounds:
- With an API, the first thing that happens on the wire is a call.
- With MCP, the first thing that happens on the wire is a question.
Before invoking anything, the client asks the server: what can you do? The server answers with a list. Only then does the model pick something from that list and call it.
Discovery first, invocation second. That ordering is the protocol's whole reason to exist.
1. client โ server tools/list "what can you do?"
2. server โ client [ 8 tools, each with a description and a schema ]
3. client โ server tools/call "do this one, with these arguments"
4. server โ client the result
Steps 1 and 2 are the part your REST API has never had.
The thing every REST developer notices first
Open the traffic and something looks wrong: there is no resource path.
Your API puts the noun in the URL and the verb in the HTTP method โ GET /invoices/42, DELETE /invoices/42. MCP does neither. There is one path, /mcp, and every single message is a POST to it.
That is because MCP speaks JSON-RPC 2.0. The operation is not in the URL and not in the method โ it is in the body, in a field called method:
POST /mcp
Authorization: Bearer <token>
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
The reply is a menu โ and this is the interesting part. Each entry carries a name, a plain-English description of what it is for, and a schema in which every parameter has its own description:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "find_invoices",
"description": "Find a customer's invoices for a given year. Use this when someone asks about billing history, unpaid bills, or what a customer was charged.",
"inputSchema": {
"type": "object",
"properties": {
"customer": {
"type": "string",
"description": "Customer name or account ID, e.g. 'ACME Ltd' or 'cus_8812'"
},
"year": {
"type": "integer",
"description": "Four-digit calendar year, e.g. 2026"
}
},
"required": ["customer", "year"]
}
}
]
}
}
That is documentation, delivered as data, over the same wire the call goes out on. Nobody had to read anything in a browser.
Calling goes through the same door. Only the method changes:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "find_invoices",
"arguments": { "customer": "ACME Ltd", "year": 2026 }
}
}
And it is authenticated exactly the way your API is โ a bearer token on every request. This is not a new security model. It is your security model, on a new endpoint.
You never write that JSON by hand
Look at that schema again and the obvious objection is: nobody is maintaining that.
Correct. You do not write it. FastMCP derives it from code you were already writing:
from fastmcp import FastMCP
from pydantic import Field
from typing import Annotated
mcp = FastMCP("billing")
@mcp.tool
def find_invoices(
customer: Annotated[str, Field(description="Customer name or account ID, e.g. 'ACME Ltd' or 'cus_8812'")],
year: Annotated[int, Field(description="Four-digit calendar year, e.g. 2026")],
) -> list[dict]:
"""Find a customer's invoices for a given year.
Use this when someone asks about billing history, unpaid bills,
or what a customer was charged.
"""
return billing.query_invoices(customer=customer, year=year)
Three mappings, and that is the whole trick:
| What you write in Python | What the model receives |
|---|---|
Function name find_invoices
|
The tool name
|
| The docstring | The tool description โ what it is for and when to reach for it |
Type hints + Field(description=...)
|
The inputSchema, one described parameter at a time |
Which has a consequence worth sitting with for a second.
Your docstring is no longer a comment. It is a prompt.
It is no longer read by a teammate skimming the file. It is read by the thing deciding whether to call your function at all, and with what. A vague docstring is now a runtime failure mode, not a code-review nit.
The part that is not generated: the tools themselves
Here is where most first MCP servers go wrong, and it is the reason this article exists.
Because FastMCP generates the schema, it is tempting to conclude that the whole job is generated โ point something at your OpenAPI spec, get an MCP server out, ship it. You will get a server. You will not get a usable one.
The schema is generated. The tool design is not.
MCP is not a thin 1:1 layer over your existing endpoints. You write a new tool per capability, and the good ones are shaped like tasks, not like resources.
That distinction is concrete. Your REST API is decomposed the way a database is: /customers, /invoices, /line-items, each with filters, each returning a page. Answering "what did ACME pay us in 2026?" takes three calls and some glue โ the human client author wrote that glue once and forgot about it.
A tool has no glue author. So the tool is the whole task:
| Auto-generated from the spec | Written by hand | |
|---|---|---|
| Unit of design | One tool per endpoint / per resource | One tool per task a user actually asks for |
| Typical count | 60+ | 8 |
| Answering "what did ACME pay in 2026?" | 3 chained calls, IDs threaded by the model | 1 call: find_invoices(customer, year)
|
| Descriptions | Inherited from a spec written for humans, or empty | Written for the decision the model has to make |
| Parameters | Every filter the endpoint supports, most of them noise | Only what the task needs |
| Failure mode | Model picks the wrong one of six similar tools, or gives up | Model picks correctly, because the choice is obvious |
Point a generator at your OpenAPI spec and you get 60 tools nobody can use. Write 8 by hand, with real docstrings, and the model uses them correctly.
Why this bites harder than it looks (the AI-engineering angle)
If you have only built request/response systems, the cost of a bad tool surface is not obvious. It is not a latency problem. It is a context and decision problem, and it shows up in three places.
Every tool is permanently in the prompt. The tool list is not fetched when needed โ it is serialised into the model's context on every single turn of the conversation. Sixty tools with full schemas is thousands of tokens spent before the user has said anything, on every message, forever. Eight task-shaped tools cost a fraction of that, and leave the budget for the actual work.
Every extra tool is another chance to choose wrong. Selection accuracy degrades as near-duplicates pile up. Six endpoints that all list something, with descriptions inherited from a spec written for a human who already knew which one they wanted, is a menu designed to be misread. This is the same reason a chained workflow is fragile: three dependent calls means three chances to thread the wrong ID, and one wrong ID is a confidently wrong answer rather than an error.
The description is the interface. In a normal API, the contract is the signature and the docs are advisory. Here the prose is the routing logic โ a tool whose description does not say when to use it will be called at the wrong moment, no matter how correct its implementation is. Write descriptions that answer the model's actual question ("is this the one?"), not the ones that restate the function name.
Which gives you a useful design test. For each tool, ask: is this a thing a user would ask for in a sentence? find_invoices(customer, year) passes. list_line_items(invoice_id, page, per_page, sort) does not โ it is an implementation detail that leaked onto the menu.
The verdict
Adding MCP is not rewriting your product, and it is not flipping a switch either.
Keep everything below the surface: your queries, your business logic, your auth, your database. None of it moves. The REST API stays exactly where it is, serving the clients it already serves โ MCP does not replace it, it sits beside it.
What is genuinely new is a tool surface, and that surface is a design artifact you author. The protocol gives you discovery. FastMCP gives you the schema. The judgment about which capabilities exist and what shape they have is the part that is still yours, and it is the part that decides whether the thing works.
The short version, if you take one line away: the schema is generated for you; the tool design is not. Write eight tools shaped like the questions people ask, give each a docstring you would be happy to have read aloud as an instruction, and you are done.
References and further reading
On the protocol itself โ discovery, and the JSON-RPC envelope
- Model Context Protocol, Specification โ the normative definition of
tools/listandtools/call, the tool object with itsdescriptionandinputSchema, and the transport and authorization sections behind the bearer-token point above: modelcontextprotocol.io/specification - JSON-RPC Working Group, JSON-RPC 2.0 Specification (2010) โ the envelope MCP is built on: why the operation lives in a
methodfield inside the body rather than in a URL or an HTTP verb, and howid,params,resultanderrorfit together: jsonrpc.org/specification
On why a REST API looks so different โ the noun in the URL
- Roy T. Fielding, Architectural Styles and the Design of Network-based Software Architectures (PhD dissertation, UC Irvine, 2000), ch. 5 โ the resource/identifier/uniform-interface model that puts the noun in the path and the verb in the method, which is exactly the convention MCP declines to use: ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
- OpenAPI Initiative, OpenAPI Specification โ what an endpoint-shaped description of a service actually contains (paths, operations, parameters), and therefore what a generator pointed at one can and cannot know about the tasks a user wants to perform: spec.openapis.org/oas/latest.html
On generating the schema from code
-
FastMCP documentation โ the
@mcp.tooldecorator and the derivation rules used above: function name to tool name, docstring to description, type hints toinputSchema: gofastmcp.com - Pydantic documentation, Fields and JSON Schema โ how
Field(description=...)and annotated types become the per-parameter descriptions the model reads, which is where most of the usable signal in a tool schema comes from: docs.pydantic.dev/latest/concepts/fields/
On the part that is not generated โ designing the tools
- Anthropic, Writing effective tools for agents (Anthropic Engineering, 2025) โ the case for consolidating several low-level endpoints into one task-shaped tool, for spending real effort on tool descriptions, and for evaluating a tool surface rather than assuming it: anthropic.com/engineering/writing-tools-for-agents
If a reference you would expect is missing, say so in the comments and I will add it.
Watch the short: MCP vs API โ Why Your Server Needs a /mcp Endpoint
Keep going: the rest of the free AI-engineering course lives at software-engineer-blog.com/ai.
Top comments (0)