Contact

Building an MCP server for internal tools, from tool design to OAuth

Etching of a physicist's workshop in 1791: at three long tables people work an electrical machine with a large wheel, glass jars, pulleys and a hanging disc, each at their own instrument, with cords running between the apparatus and the ceiling.
Lesson in the workshop of a natural philosopher, Anonymous, 1791. Rijksmuseum, public domain.

A support lead asks the AI assistant her company already pays for: what is the status of order 4471, and did the refund go through? The answer sits in three places, an order database, a billing system and a ticket tool, and the assistant can see none of them. A Model Context Protocol server is the standard way to give it that access, once, for every assistant and agent the company uses. The first version most teams build wraps the REST API one endpoint at a time, and the assistant uses it badly. This is how we approach one instead.

Engraved plate of a records office: behind a wooden counter, a clerk hands one tied orange bundle of papers to a visitor in a tricorn hat; behind the clerk, a wall of small drawers with three pulled open, where the papers came from.
Illustration in the manner of an engraved plate, generated for this page.

What an MCP server is, in the current specification

The current revision of the protocol is 2026-07-28. Messages are JSON-RPC. A host, such as a chat assistant or a coding agent, runs a client for each server it connects to, and the server is the program you write in front of your own systems.

A server can offer three kinds of thing, and the specification separates them by who controls them. For internal tools, nearly all the work is in the tools.

Primitive Controlled by What it is Useful for
Tools the model functions the model decides when to call nearly all the work
Resources the application context the client attaches, such as a policy document or a database schema reference material that rarely changes
Prompts the user templates a person picks, usually as a slash command a workflow people repeat, such as “summarise this customer’s last month”

There are two standard transports, and the choice follows from who uses the server.

stdio Streamable HTTP
How it runs the client launches the server as a subprocess on the same machine; they exchange newline-delimited messages the server exposes one endpoint; each message is a POST, and the reply is a JSON object or a short event stream scoped to that request
Suits a tool for one developer’s laptop anything a team shares
Credentials from the environment, as the authorisation specification says each person’s identity reaches the server

The 2026-07-28 revision removed the initialisation handshake and protocol-level sessions, so every request carries its own protocol version and a remote server can sit behind an ordinary load balancer. State that has to survive between calls, such as a draft refund, becomes an explicit handle the tool returns and the model passes back.

A shared server runs over HTTP because that is where each person’s identity reaches the server and where there is one place to deploy, log and revoke.

Design tools for the model, not for the API

The mirrored API fails in a predictable way. Forty endpoints become forty tools, and every tool’s name, description and schema sits in the model’s context on every turn. To answer the support lead, the model has to find the customer, list their orders, fetch the order, fetch the payments and fetch the refunds, choosing correctly at each step between tools with near-identical names. Each step is a chance to pick the wrong one.

Anthropic’s guide to writing tools for agents puts it plainly: build a few thoughtful tools aimed at specific high-impact workflows rather than wrapping every endpoint, prefer a search tool to a list tool, and fold multi-step operations into one call. Start from the questions people actually ask. Read a month of the support inbox and the team chat, write down every question that needed someone to open an internal system, and group them. For an order business, a handful of tools usually covers most of the list: find a customer by name, email or phone; get an order’s status with its payment, shipment and refunds in one answer; search tickets; read a product’s stock. The server calls five internal endpoints to answer one of those. The model sees one tool.

one tool per endpoint model internal apis model calls one task-shaped tool model server internal apis model calls
One tool per endpoint costs the model five calls to answer one question; one task-shaped tool costs it one, and the server makes the other calls.

The description is the part the model reads, so write it for a new colleague on their first day. Say what the tool is for, when not to use it and what the identifiers look like. Name parameters so they cannot be misread, customer_id rather than user, and use enums wherever the value comes from a fixed list.

{
  "name": "get_order_status",
  "title": "Order status",
  "description": "Current status of one order: items, payment, shipment and any refunds. Use when someone asks where an order is or whether it was refunded. Takes an order number such as 4471, not a customer name; call find_customer first if you only have a name.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "order_number": { "type": "integer", "description": "The number printed on the invoice" },
      "detail": { "type": "string", "enum": ["summary", "full"], "default": "summary" }
    },
    "required": ["order_number"],
    "additionalProperties": false
  },
  "annotations": { "readOnlyHint": true, "openWorldHint": false }
}

Outputs should be small and written for reading. Return the customer’s name and the order’s status in words, not three UUIDs and an internal status code. A detail parameter lets the model ask for the short version by default and the full record when it needs it. Anything that returns many rows takes a limit with a modest default, returns a cursor for the next page and says how many rows there are in total, so the model can narrow the search instead of paging through everything.

Errors are part of the interface. The tools section of the specification distinguishes protocol errors, such as an unknown tool, from tool execution errors, which come back as a normal result with isError set. The second kind is meant for the model to read and correct itself, so the message should say what to do: “No order 44710. Order numbers have four digits; did you mean 4471?” is worth more than a stack trace.

Read-only first

The first release should only read. It is useful on day one, because most of what people ask internal systems is a question, and it gives you weeks of logs showing how the model uses the tools before any of them can change anything.

The specification defines annotations such as readOnlyHint and destructiveHint, and clients can use them to decide when to ask the person for confirmation. They are only hints: clients must treat annotations as untrusted unless the server is trusted, and nothing in the protocol stops a tool marked read-only from writing. Enforce read-only where it cannot be argued with.

Control Enforced by Stops a write?
readOnlyHint and destructiveHint annotations the client, if it trusts the server no, they are hints
a database role with no write grants the database yes
API credentials that cannot create or delete the internal API yes

Writes come later, one narrow tool at a time: a refund tool that creates a draft for a person to approve, capped at an amount, rather than a general “update order” tool. How far and how fast to widen an agent’s permissions is its own subject, covered in putting an AI agent inside a workflow you already run.

Per-user permissions, OAuth and audit logs

A shared server should act as the person using it, not as a service account that can see everything. For HTTP servers the specification uses OAuth 2.1. The MCP server is a resource server: it publishes Protected Resource Metadata (RFC 9728) so clients can find the authorisation server, clients name the server they want a token for using resource indicators (RFC 8707), and the server must reject tokens that were not issued for it. The authorisation server can be the identity provider the company already uses for single sign-on. Check early that it supports a client registration method your assistants can use: the specification prefers Client ID Metadata Documents, allows pre-registration and keeps Dynamic Client Registration only for compatibility. This is where projects stall, so it belongs in the first week, not the last.

The specification allows the tool list itself to vary with the caller’s authorisation. A support agent’s assistant does not see the refund tool at all, so the model cannot be talked into calling it. Behind each tool, the server checks the person’s permissions against the record they asked for, on every call. Scopes start minimal. When a call needs more, the server answers with a 403 that names the missing scope, and the client asks the person to grant it then, rather than requesting everything up front.

Do not forward the person’s token to the internal systems. The security best practices forbid this pattern, called token passthrough, because a token accepted by several services lets anyone who steals it reach all of them, and the downstream logs no longer show who asked. The server authenticates to the internal systems with its own credentials and applies the person’s permissions itself.

internal systems assistant client user token mcp server user check orders billing tickets audit log
The client sends the person's own token, the server checks their permissions, calls the internal systems with its own credentials and writes every call to the audit log.

Every tool call goes into an audit log: the person from the verified token, the client, the tool, the arguments, the identifiers of the records returned, the latency and whether it succeeded. Over Streamable HTTP the client also mirrors the method and the tool name into Mcp-Method and Mcp-Name headers, so a gateway in front of the server can log and rate-limit per tool without parsing the body. The log answers the question someone will eventually ask, which is who looked at a customer’s record and why. We described building that kind of access log in consent and access logs in a health platform.

Tool results also carry text the model will read, and a ticket or an email can contain instructions written by an outsider. That risk, and what to do about it, is in prompt injection defences for tool-using agents.

Testing with the MCP Inspector

The MCP Inspector is the reference tool for exercising a server without a model. npx @modelcontextprotocol/inspector node build/index.js opens a browser interface where you can list the tools, call one with hand-written arguments and see the JSON-RPC exchange. The same package has a --cli mode that prints machine-readable output, which makes it easy to run in CI.

The Inspector tests the protocol, not the model’s judgement. Whether the model picks the right tool for “did 4471 get refunded?” is a separate test.

Inspector in CI Evaluation set
Tests the protocol the model’s judgement
Input tool lists and hand-written arguments, as two different test users real questions from the inbox, with the tool calls and answers a person agrees are right
Checks the two users see different tool lists; a bad argument gets an error that says how to fix it; a summary response stays under a size you have chosen tool choice and answer, through a real client and model
Runs on every build on every change to a description or schema

How to build that set is in building an evaluation set from past cases.

Versioning the protocol and the tools

There are two versions to manage. The protocol version travels on every request, and a server that does not support the requested version answers with an error listing the versions it does support. The SDK handles most of this. The 2026-07-28 revision was a breaking one, and clients that speak only the older, session-based revisions cannot fall forward, so pin the SDK version and decide on purpose whether the server also serves older clients.

The tools are the harder half, because their consumers are prompts and habits rather than compiled code. Renaming a tool or changing a parameter does not fail a build; it quietly changes what the model does. Treat descriptions as code: reviewed, versioned and run against the evaluation set before release. An incompatible change gets a new tool name, the old one stays for a stated period, and clients that subscribe are told the list changed. The specification asks servers to return tools in a stable order, which keeps the tool list cacheable and helps prompt caching on the model side.

When an MCP server is the wrong answer

Situation Use Why
one application you own calls a model with three functions, and both sides live in your codebase the model provider’s function calling directly MCP adds a process, a protocol and an authorisation layer that nothing reuses; the functions can move behind an MCP server later if a second consumer appears
the clients are not yours: the assistants your staff already use, coding agents, a partner’s agent, or several of your own products that need the same tools an MCP server this is where MCP pays
a batch job that extracts the same fields from every incoming document code calling a model with a fixed schema, as in reading supplier invoices with an LLM nothing in that job needs a model to choose a tool

A useful first version is small. Pick the internal system people ask about most, write three read-only tools that answer the questions in last month’s inbox, put them behind your existing sign-on, log every call, and watch the log for two weeks before adding anything. The AI agents and automation page describes how we take on this kind of work, and you can write to us with the systems you want an assistant to reach.

More writing

  1. AI customer support automation: what to automate first, and what to keep

    AI customer support automation in order: triage, drafted replies, cited answers, account lookups, what stays with people, and measuring real resolution.

  2. Keeping a fleet of wearables synced through phones that want to sleep

    BLE reconnection, iOS background modes and Android Doze, device buffering, idempotent uploads, clock drift, and fleet monitoring that catches a quiet device.

  3. Building an evaluation set from past cases, and keeping it alive

    How we build and keep an evaluation set for an LLM feature: choosing cases, labelling with the people who do the job, size, and growing it from production.