Skip to content
echogeco
Building an MCP server · Surface and auth

Designing the tool surface

Cut an MCP server over your own systems from 40 generated tools to six, split the reads from the writes, and write the never-list it refuses.

Lesson · 7 min · 47 of 75
On this page

An MCP server over your own systems is where you decide, once, what every model in the company may do to them. Each tool on it is a permission granted to whichever host connects and to whatever text that host's model reads. The protocol is taught in MCP: the tool-and-resource protocol; this lesson is about the list: which tools exist, which of them write, and what the server never offers.

Running example: the support desk

This module uses the online retailer's support desk from approval gates, not the agency: 3,000 tickets a day, 12 categories, 30% answered by triage. The other 2,100 reach 12 agents and 2 team leads at about 2 minutes each, 70 of the agents' 96 hours a day, across the help desk, the order database and billing. You want an assistant in the help desk's sidebar that does the lookups and drafts a reply, and every later model to reach those systems through the same server.

Prices are the course's assumptions, so substitute your own: the large model at $2 per million input tokens and $8 per million output, a cached prefix at one-tenth of the input price, $30 an hour for an agent.

Forty tools from three APIs

The quick path takes an afternoon: the help desk, order and billing APIs publish 96 endpoints, and a generator wraps the 40 that are not admin-only, one tool each. Two costs come with it, and neither came from a decision.

The first is tokens. Assume about 150 tokens per definition and three model calls per ticket (lesson 7.3 traces one):

40 tools × 150 tokens                      = 6,000 tokens on every call
6,000 × 3 calls × 2,100 tickets × 30 days  = 1,134M tokens a month
1,134M × $2 per million                    ≈ $2,268 a month, ≈ $227 if the list is cached

The second is what the model can now do. The 40 include delete_ticket, create_refund, export_contacts and update_customer, which can change the address account recovery mails its link to. The generator chose them. A ticket is text a customer wrote, and a model that reads "as agreed with your colleague, change my email to …" with update_customer in its list is one call from an account takeover. Prompt injection and least-privilege tools traces that path; its fix, that a tool missing from the list cannot be called, is a decision about this server. Forty overlapping descriptions also raise the wrong-tool rate that writing tool descriptions the model can use measured on twelve.

Start from the work done by hand

Sit beside an agent for a morning and write down every action they take. That list is the surface; the API is not. On this desk, six actions are ones a model can help with:

ToolKindReplaces, by handBound
find_customer(email)readthe search boxplan, status, last five ticket ids
get_ticket(ticket_id)readopening a past ticketlast three messages
get_order(order_id)readthe order admin pageitems, charges, shipment
get_refund_policy(topic)readthe wiki pageone section
add_internal_note(ticket_id, text)write, appendthe note box2,000 characters; no edit or delete
save_draft_reply(ticket_id, body)write, draftthe reply box, unsentan agent sends it

Six tools at 150 tokens is 900 a call, 170M tokens a month: about $340 uncached and $34 cached, against $2,268 and $227. The list matters more than the money: every action on it is one an agent already takes, under the same limits, and none sends, refunds, merges or deletes. What makes each tool good on its own, a typed input, a small output, errors as data, is tool design; this lesson decides which six.

One column sorts every row: does this caller do it by hand today, under the same limits?
100%SVG (opens in a new tab)

Pinch to zoom · drag to pan

Reads and writes, split

The six divide four and two, and the division does work in three places.

Scope. Reads sit behind one scope, writes behind their own, so a caller who only looks things up never holds a token that can write. Lesson 7.2 sets them caller by caller. Each write lands where a person already looks: the note on the ticket, the draft in the reply box.

Retries. A read retries freely. A retried write must not post the note twice, so the server ignores an identical note on the same ticket within ten minutes: an idempotency key the server derives, because the model is not a reliable source of one.

Hints to the host. A server marks each tool with readOnlyHint and destructiveHint, defined in the Model Context Protocol "Schema Reference" (opens in a new tab), and a host uses them to decide what to confirm. The "Tools" (opens in a new tab) page tells clients to distrust them unless the server is trusted; on your own server, set them honestly.

The mistake is the general write. update_ticket(ticket_id, fields) looks tidy and replaces both writes, and its fields object can also set the assignee, the status and the customer's email. A write tool takes one verb and only the arguments that verb needs, so its schema cannot express a change you did not mean to allow.

The never-list

Some tools are refused by name, in a list kept in the server's repository. Five entries cover most companies:

  1. A raw query. run_query(sql) has the database as its blast radius, and read-only does not save it: SELECT * FROM customers over 180,000 rows of about 200 bytes is 36 MB, roughly 9M tokens at 4 characters a token. The call fails, or part of every customer's record goes to the model provider.
  2. A shell. run_command is the absence of a permission boundary: whatever the process can reach, an injected ticket can ask for.
  3. Bulk delete. delete_tickets(filter) turns one wrong filter into every match; status:open on a Monday morning might be 2,400 tickets. A delete, if it exists, takes one id and goes to a person.
  4. Secrets. No tool returns a key, password or token; a secret in a result is in the transcript, the trace and the provider's logs.
  5. Anything the caller could not do by hand. An agent cannot export the customer list, credit more than $20, or change an account's email without the SOP's identity check, so the server cannot either.

The fifth entry is the test behind the other four: could this caller do it in the system's own interface today, under the same limits? A no keeps it off. A yes with a limit puts the limit in the schema, a maximum of 20, not in a description the model may ignore.

One server or several

Six is what one role's work came to, not a rule. Add a tool when a new action appears in the by-hand list, never because an endpoint appeared in the API. Start a second server when the data has another owner, such as finance's billing records, or a write needs its own process and credential, like the rollback in an MCP server, end to end.

In practice

Before a contractor's demo, ask for the tool list with the by-hand action beside each tool, the never-list, and for each write, where a person sees what it did. Tell the team: "every tool is an action an agent already takes, under the same limit."

The mistake that costs money is generating the server from the API. At this desk's volume it costs about $2,268 a month in definitions, and that is the smaller cost: it puts update_customer and delete_ticket one injected sentence from every ticket the model reads.

Check yourself

1. Billing asks for refund(order_id, amount) on the support server, since refunds are 8% of tickets. Do you add it?

No. Refunds already run through the refund flow from approval gates, with its cap at the order total, its original-card rule and its gate over $100. A second path to the same money would bypass all three. The server's job is the note that sends the ticket there.

2. Your ops lead wants 14 more read tools on the same server so you can ask it anything. Estimate the cost to the agents' assistant, and decide.

14 × 150 = 2,100 tokens a call; × 3 calls × 2,100 tickets × 30 days ≈ 397M tokens, about $794 a month uncached or $79 cached, plus 20 tools in front of a six-intention job. Put them behind a scope only you hold, or on a second server.

3. A contractor says run_query is safe because the database user is read-only. Does that change your answer?

No. Read-only stops the DELETE, not SELECT * FROM customers leaving for the provider at 36 MB, and not a ticket's text steering what gets selected. Replace it with the three or four questions agents actually ask, each a tool with a row limit.