A worked server: the support desk
Specify the support desk server as five tools, two resources and one prompt, trace one ticket through it, and account for its 3,100 tokens a call.
On this page
The support desk server from the last two lessons, the online retailer's from module 4 and not the agency's, comes to five tools, two resources and one prompt, behind three scopes, over Streamable HTTP. The handlers an SDK writes for you are shown in an MCP server, end to end. This lesson gives the parts specific to the desk, traces one ticket through them, and accounts for every token it costs.
The surface, and the one change
| Primitive | Name | Chosen by | Scope | Returns |
|---|---|---|---|---|
| prompt | triage_ticket(ticket_id) | the agent, as /triage | desk:read | 700 tokens of instructions plus the open ticket |
| resource | desk://tickets/{ticket_id} | the host | desk:read | the thread's last three messages, about 1,000 tokens |
| resource | desk://policies/refunds | the agent, from a picker | desk:read | the refund and credit policy, about 1,100 tokens |
| tool | find_customer(email) | the model | desk:read | plan, status, last five ticket ids |
| tool | get_ticket(ticket_id) | the model | desk:read | a past ticket's last three messages |
| tool | get_order(order_id) | the model | desk:read | items, charges, shipment, about 150 tokens |
| tool | add_internal_note(ticket_id, text) | the model | desk:note | the note's id |
| tool | save_draft_reply(ticket_id, body) | the model | desk:draft | the draft's id; never sends |
One thing changed from the six tools of designing the tool surface. get_refund_policy(topic) returned the same text whenever it was called, and that text then rode in the history for the rest of the ticket. A read whose answer is fixed, or follows from which ticket is open rather than from the model's reasoning, is a resource. The host offers the policy in its picker and attaches it when the agent chooses to, about one ticket in ten. The open ticket follows the same rule: it is a resource template, which the prompt embeds. get_ticket stays a tool because reading a customer's earlier tickets is a choice the model makes. That is the split between primitives from the protocol lesson, applied to one desk.
The prompt is where the SOP lives. The Model Context Protocol "Specification" (opens in a new tab) describes prompts as templated messages and workflows for users, and this one is the desk's procedure. Its 700 tokens say what to look up, what the note must contain, how the draft should sound, and the two rules agents apply every day: a duplicate charge is refunded in full, and a goodwill credit stops at $20. All 14 people on the desk run the same procedure, and changing it is one server deploy rather than 14 edits to a saved snippet.
Two excerpts
A runnable server needs an SDK, and the SDK's registration calls change between releases, so here are the two pieces that are yours whichever SDK you use. The first is one definition, as the client receives it from tools/list:
{"name": "save_draft_reply",
"description": "Save a reply draft on a ticket for the agent to review and send. Never sends. Use once you have the facts. Saving the same body twice keeps one draft.",
"inputSchema": {"type": "object",
"properties": {"ticket_id": {"type": "string", "pattern": "^T-[0-9]{5}$"},
"body": {"type": "string", "maxLength": 1500}},
"required": ["ticket_id", "body"], "additionalProperties": false},
"annotations": {"readOnlyHint": false, "destructiveHint": false, "idempotentHint": true}}
There is no recipient argument, so the draft can only go to the ticket's own customer, and only after Priya presses send. The second piece is the path every call takes through the server, built from lesson 7.2:
def handle_tool_call(request):
caller = verify_token(request, audience=SERVER_URL) # 401 unless issued for this server
tool = TOOLS[request.name]
if tool.scope not in caller.scopes:
audit(caller, request, outcome="denied")
raise InsufficientScope(tool.scope) # 403, names the scope needed
upstream = help_desk_grant(caller.sub) # the caller's own help desk token
result = tool.run(upstream, request.arguments) # schema checked inside, errors as results
audit(caller, request, outcome=result.outcome) # one entry per call, before returning
return result
One ticket, call by call
Ticket T-58113 reads: "I was charged twice for March on order SD-48213. Please fix this." Priya opens it and types /triage. The host calls prompts/get, and the server returns the instructions with the ticket embedded. The host's 500-token system prompt and the five definitions sit in front of them on every call; the definitions measure about 600 tokens, 120 each, under the 150 lesson 7.1 assumed because each description is short.
| Call | In | Out | What happens |
|---|---|---|---|
| 1 | 2,800 | 40 | The model asks for get_order("SD-48213"). The server checks Priya's token and desk:read, reads the order as Priya, and returns 150 tokens: two $49 captures, 11 seconds apart. |
| 2 | 2,990 | 260 | The model calls two tools in one turn: a note ("duplicate capture, 2 × $49 on 3 March, 11 s apart; refund the second") and a 200-token draft telling the customer the second charge will be refunded. Each call writes an audit entry under priya@. |
| 3 | 3,290 | 20 | Both results come back at about 20 tokens each, and the model ends: draft ready, refund to start. |
Nothing on the surface can refund the $49, by design. Priya starts the refund with one click in the help desk, which hands it to the refund flow from approval gates with its cap, its original-card rule and its gate over $100; the server needs no second path to the same money. She reads the draft, changes one word and sends it.
Where the 3,100 tokens go
The three calls total 9,080 tokens in and 320 out, 9,400 in all, or about 3,100 a call.
The definitions, the system prompt and the instructions are byte-identical on every call, 1,800 tokens, as long as the server lists its tools in the same order every time, which prompt caching depends on. With the course's assumed prices:
cached prefix 3 × 1,800 = 5,400 at $0.20 per million $0.0011
other input 9,080 − 5,400 = 3,680 at $2 per million $0.0074
output 320 at $8 per million $0.0026
per ticket ≈ $0.011
per month 2,100 tickets × 30 days × $0.011 ≈ $693 (≈ $1,305 uncached)
Against the people it serves, that is small. Assume the assistant saves an agent one minute a ticket: 2,100 minutes a day is 35 hours, about $1,050 a day at $30 an hour, so the month's token bill is about two-thirds of one day's saving.
The server's choices show up in the same arithmetic. The 40 generated tools from lesson 7.1 would add 5,400 tokens of definitions to every call, 16,200 a ticket, and almost triple it to 25,600. Returning the order system's raw record, about 1,500 tokens, instead of the 150-token summary would add 1,350 tokens to calls 2 and 3, 2,700 a ticket: 170M tokens a month, about $340.
In practice
Accept the server on a transcript, not a demo. Ask the contractor for one real ticket traced call by call with the tokens in and out, the tool calls with the identity each ran as, and the audit lines they wrote. Then say what you will measure: tokens per call, with this desk's line at 3,100, and calls per ticket, at 3.
The mistake that costs money is accepting a server because it answers. The generated server answers too, at 25,600 tokens a ticket, and a server that passes raw records through answers at an extra $340 a month. Only the transcript shows either.
Check yourself
1. Leads want the refund policy attached to every billing ticket automatically. Billing is 30% of the 2,100 tickets. Estimate the cost and decide how to attach it.
630 tickets × 3 calls × 1,100 tokens ≈ 2.1M tokens a day, 62M a month, about $125 uncached. Attach it through a
triage_billing_ticketprompt that places the policy before the ticket, so it joins the cached prefix for those calls: about $12 a month. Do it, because a wrong credit costs more than that.
2. The trace shows the model calling find_customer and get_ticket before every get_order, two extra calls a ticket. Estimate the effect and say what you change.
Two extra calls at about 3,000 tokens each, plus about 1,200 tokens of their calls and results, a customer card and a past ticket's three messages, riding on the three later calls: 9,400 + 6,000 + 3,600 ≈ 19,000 a ticket, about double. Put the customer card in the ticket resource for about 120 tokens, and have the instructions say the order id is in the ticket, so the model has no reason to look.
3. The contractor proposes merging add_internal_note and save_draft_reply into one update_ticket to save about 120 tokens of definitions. Decide.
No. 120 tokens × 3 calls × 63,000 tickets is about 23M tokens a month: about $45 uncached, under $5 cached. The merge loses the split between
desk:noteanddesk:draft, and a schema in which one verb carries only its own arguments.