Software Engineering · Chapter 31 of 31
Building an MCP server in an enterprise
What the job market actually asks for, how MCP design differs from API design, and the decisions that separate a demo server from one you can run.
The previous chapter covers what MCP is and shows a thirty-line server. This one is what changes when it has to run inside a company — and it starts with the job market, because the premise is worth checking.
First, what the postings actually say
The honest read, if you are hearing about “MCP Engineer” roles
MCP appears in 2.9% of postings — 86 of 3,010. That is genuinely remarkable for a protocol under two years old, and it is already ahead of gRPC at 1.7%.
But it appears in ONE job title, and that one is a product-management role. There is no established “MCP Engineer” job.
API is the same shape, further along: 8.8% of descriptions, 0.5% of titles.
So the preparation advice inverts the intuition. Do not chase a title that barely exists — the demand is real and it arrives inside backend, platform and integration roles that expect you to build these things. Being the person who has built one is worth considerably more than a title that has not settled yet.
And notice where API IS a title: API Safety, API SDK, API Agents, API Enterprise Controls. Those are AI labs, where the API is the product being sold. That is a different job from building your bank's internal APIs, and conflating them will mis-target your preparation.
How MCP design differs from API design
The transport, the auth and the hosting are ordinary web engineering. What changes is the consumer — and it changes every design decision downstream.
Two terms, in case they are not familiar. A REST API is the ordinary web interface most systems expose — a URL identifies a thing, and the HTTP method says what to do with it. An OpenAPI specification is its machine-readable description: every endpoint, parameter and response shape, in a file. MCP is the equivalent description aimed at a model rather than a programmer — which is exactly why generating one from the other does not work.
Take the granularity row, because it is the one that decides whether the thing works. A REST API exposes GET /customers/{id}, GET /customers/{id}/orders, GET /orders/{id}/items, and a developer composes them. An agent composing three calls burns three round trips, three sets of tokens, and three chances to pick the wrong one. One tool called get_customer_order_history(email) that does the joins server-side is a better tool, even though it would be a worse REST API.
What you actually have to decide
| Decision | The enterprise answer |
|---|---|
| Transport | Local subprocess is fine on a laptop. Inside a company it is an HTTP service — deployed, monitored, versioned like anything else. |
| Who is the caller? | The hard one. Not the model — the person the model is acting for. See agent authorisation: a shared service account hands every user the permissions of the most privileged one. |
| Tool surface | Few and job-shaped. Past roughly fifteen to twenty, selection accuracy degrades — group them, or load only what this task needs. |
| Response shape | Bounded by construction. A limit the server enforces, and the six fields that matter — everything returned is paid for on every subsequent turn. |
| Errors | A sentence naming the next step. The agent reads it and decides what to do; a stack trace makes it retry identically. |
| Discovery | A registry. Otherwise you get six teams' servers, three of which do the same thing, and nobody can enumerate them. |
| Versioning | Renaming a tool or changing a description is a behaviour change, not a cosmetic one. Treat descriptions as API surface. |
The decision that most often stops an enterprise MCP server going live, and it is not technical.
Whose credentials does it use?
Almost every tutorial uses one service account with a token in an environment variable. That works immediately, and it means every user of the agent inherits the permissions of that account. If it can read the HR space, so can everyone who can reach the tool.
The correct answer is per-user delegation — token exchange, so the server calls the downstream system as the asking person and the audit log names both. It is a materially harder build, and it is the difference between a pilot and a production system. Covered in authentication and authorisation for AI agents.
Decide this before you write the server, because retrofitting it means changing every tool and every deployment.
# The enterprise shape. The plumbing is ordinary; the marked lines are not.
@mcp.tool()
def find_orders(customer_email: str, status: str | None = None, limit: int = 20) -> str:
"""Find a customer's orders. Returns order ID, date, status, total and item count.
Use this when the user asks about an order's status or purchase history.
Do NOT use it to modify an order — use update_order for that.
"""
# ^ THE DOCSTRING IS THE API. It is what the model reads to decide whether
# to call this and with what. Changing it changes behaviour: version it.
user = current_user() # 1. WHO is this actually for?
token = exchange_token( # 2. downscope, per call
subject=user.token, resource="orders-api", scope="orders:read")
limit = min(limit, 50) # 3. bounded by the SERVER,
# never by trusting the model
try:
rows = orders_api.search(email=customer_email, status=status,
limit=limit, token=token)
except NotFound:
# 4. errors are prompts — say what to do next, not what broke
return (f"No customer found with email {customer_email}. "
f"Use find_customer(name) to look up their address first.")
except Forbidden:
# 5. and NEVER leak existence you have not authorised
return "You do not have access to this customer's orders."
log.info("mcp.find_orders", actor=user.id, agent=agent_id(), # 6. both IDs
count=len(rows))
return format_rows(rows, fields=("id","date","status","total","items"))
# ^ 7. six fields, not fortyThe operational parts people forget
- It is a production service. Health checks, deploys, on-call, an SLO. An MCP server that goes down takes every agent depending on it with it, and nobody will have put it in the dependency map.
- Rate limits belong on the server. An agent in a loop will call your tool far faster and far more repetitively than a human ever would.
- Log the actor chain, not just the call. Which agent, for which person, with what arguments. Without it an incident is uninvestigable — the same point as the audit line in the agent-authorisation chapter.
- Test the descriptions, not only the functions. Your unit tests prove the code works; they say nothing about whether a model picks the right tool. Add a small evaluation set of realistic requests and assert which tool gets chosen — see the eval chapter. This is the test nobody writes and the failure everybody has.
- Publish it somewhere findable, with an owner. The registry problem arrives faster than you expect — see enterprise agent platforms.
Should you build one at all?
A fair question, and the answer is often no.
- If one team wants one integration, a plain function in their agent is simpler and you can delete it later. MCP earns its keep when several clients need the same capability.
- If the capability is a deterministic workflow with known steps, that is automation, not a tool for a model to reason with.
- Build one when multiple agents or teams need the same system, you want the access control and audit in one place rather than replicated, and you would otherwise be writing the same integration for the third time.
- The strongest argument for a central MCP server is not reuse. It is that the authorisation and the logging live in one reviewable place instead of five.
Check yourself