Model Context Protocol solved a real problem: before it, every agent integration was a bespoke adapter, and connecting five tools to three assistants meant fifteen implementations. MCP makes the tool side a server anyone can speak to. The consequence for anyone building enterprise software is that your product now has a second interface — one consumed by models rather than people — and it will be judged on how well an agent can drive it.
I have built MCP servers for internal platforms and watched them go through the same security review as any other integration. The protocol is the easy part. What determines whether the server is useful, and whether it gets approved, is tool design and authorization.
Granularity is the whole design
The instinct is to expose your REST API one-to-one. Do not. An API designed for a frontend has endpoints shaped by screens, and an agent driving it spends five calls assembling what a human sees in one view. The opposite mistake is worse: a single do_everything tool with a mode parameter, which puts the actual decision in a free-text field where the model can invent values.
Shape tools around intents a user would name out loud. find_overdue_invoices is a good tool. query_table is a liability. The rule I use: if you cannot write the tool description in two sentences without the word 'or', it is more than one tool.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP('billing')
@mcp.tool()
def find_overdue_invoices(days_overdue: int = 30, limit: int = 20) -> list[dict]:
"""List invoices past due by at least days_overdue, newest first.
Returns invoice handles usable with get_invoice and send_reminder.
Only invoices the calling user can access are returned.
"""
user = current_actor() # from the session, never from an argument
rows = billing.overdue(user, days=days_overdue, limit=min(limit, 100))
# Return handles, not raw primary keys the model could guess or fabricate.
return [{'handle': sign(r.id), 'customer': r.customer_name,
'amount_due': str(r.amount_due), 'days_overdue': r.days_overdue}
for r in rows]Two details in that snippet matter more than the protocol wiring. The actor comes from the session rather than a parameter, so the model cannot ask for someone else's data by changing an argument. And the return value is a signed handle rather than a raw ID, so a later write call cannot be aimed at a record the agent never legitimately read.
Resources and prompts are not decoration
Tools get the attention, but resources — read-only context the client can attach — are how you stop agents from burning steps rediscovering the same facts. Expose the schema, the enum values, the tenant's configured workflow states. A prompt template shipped with the server is similarly underrated: it encodes the sequence you want agents to follow, so every client does not reinvent an approach that half-works.
Auth is where the review happens
Every enterprise security review of an MCP server I have been through asked the same four questions, and a server that cannot answer them does not get installed.
- Whose permissions does a tool call execute under, and where is that checked? The answer must be the end user, server-side, at call time.
- Which tools mutate state, and can they be disabled independently of the read tools?
- What is logged — arguments, results, actor, correlation ID — and how long is it kept?
- What happens when a document the agent reads contains instructions? Untrusted content must never carry authority.
An MCP server is not an API wrapper. It is a permission boundary that happens to speak JSON-RPC.
Scope tokens per tool rather than per server, so a client that only needs to read invoices cannot be tricked into sending reminders. And make the destructive tools two-phase — propose, then confirm with an explicit token returned by the proposal — because the difference between a helpful agent and an incident is whether send_reminder can fire without anything having approved the list it was aimed at.
The teams getting value from MCP right now are not the ones with the most tools. They are the ones whose five tools are well named, correctly scoped, and return exactly what the next step needs. That is unglamorous interface design, which is probably why it is the part most people skip.