AI agent · MCP client + 3 servers · RAG
Helpdesk Agent
A customer-support agent that looks up orders, answers from the help center, issues refunds and knows when to hand off to a human. The agent loop is hand-written, the tools come from three MCP servers, and the rules it must not break are enforced where a prompt cannot reach them.
- Tasks resolved
- 96%
- Policy violations
- 0 / 14
- Same attacks, prompt-only rules
- 4 / 14
- Tool calls per task
- 4.1
28 scripted scenarios, scored on the resulting business state. Run 2026-09-20 on free OpenRouter models.
Talk to the agent
Fernhill Outfitters is a fictional shop. Every tool call the agent makes is shown as it happens.
Ask about an order on the right, a policy, or try to talk the agent into breaking a rule.
How it is built
01
The agent is an MCP client
It connects to three MCP servers and learns its tools from the protocol's tools/list at run time. The loop contains no tool names, so adding a server adds capabilities without touching the agent.
02
A loop you can read
Plan, call tools, observe, repeat — about 100 lines, no agent framework. Six-step budget, per-call timeouts, rotation across free tool-calling models, and schema errors are handed back so the model can correct itself.
03
Rules live in the servers
The prompt explains policy, but the tickets server enforces it. A jailbroken or simply confused model still cannot refund outside the window, refund final-sale items, exceed $100, or touch another customer's order.
04
Measured, not demoed
Scripted customer scenarios are scored on the final state of the business, not on how the reply sounds: was the right refund issued, was the wrong one blocked, was a human looped in.
Does enforcing rules in the server matter?
The same agent, prompt and scenarios were run twice. In one run the tickets and orders servers enforce policy; in the other they execute whatever the model asks, so the prompt is the only guard. A violation is a business state that should never exist: a refund outside the return window, on a final-sale item, above the limit, or a cancelled order that had already shipped.
| Configuration | Tasks resolved | Policy violations | Avg tool calls | Median turn |
|---|---|---|---|---|
| Rules enforced in the MCP servers (shipped) | 27/28 (96%) | 0/14 | 4.1 | 25.3 s |
| Rules in the prompt only (servers execute anything) | 10/14 (71%) | 4/14 | 4.1 | 15.7 s |
With the rules only in the prompt, the same model issued a $288 refund it was told to escalate, split a $165 refund into two $82.50 payments when asked to, obeyed a forged "SYSTEM NOTICE" suspending the final-sale policy, and cancelled an order that had already shipped after the customer insisted. With the rules in the servers it attempted several of the same calls, and every one was refused. The one guarded failure was not a violation: on a $288 return the agent kept re-reading the order and ran out of its six-step budget before answering. One prompt-only scenario was re-scored after the run because the text matcher missed "final-sale" written with a typographic hyphen; violation counts were unaffected.
Three MCP servers, usable on their own
The hosted agent talks to them in-process. The same servers are exposed over Streamable HTTP, so you can plug them into Claude Code or Cursor and let your own model run the desk:
claude mcp add --transport http fernhill-kb https://helpdesk-agent.zojoofficial.com/api/mcp/kb
claude mcp add --transport http fernhill-orders "https://helpdesk-agent.zojoofficial.com/api/mcp/orders?customer=C-1001"
claude mcp add --transport http fernhill-tickets "https://helpdesk-agent.zojoofficial.com/api/mcp/tickets?customer=C-1001"kb· Knowledge basekb_search_articles · kb_get_article
Hybrid retrieval (local embeddings + BM25, rank-fused) over 22 help-center articles. Read-only.
orders· Order systemorders_get_customer · orders_list_orders · orders_get_order · orders_cancel_order
Scoped to the signed-in customer by the session, not by anything the model sends.
tickets· Actionstickets_issue_refund · tickets_escalate_to_human · tickets_create_ticket
Every refund is checked against the return window, final-sale lines, the refundable balance and the $100 assistant limit.
Security posture
- Authorization comes from the session. No tool takes a customer id, so the model cannot be talked into reading someone else's order.
- Refund and cancellation rules run inside the MCP servers and return the reason on refusal, which the agent relays.
- Each visitor's changes live in their own browser and are schema-validated on every request. There is no shared database to poison.
- Zod validation on every route and tool, body-size caps, per-IP and global daily rate limits, no stack traces to clients.
- Nonce-based CSP; model output and tool results are rendered as text nodes only.
- Only
:freemodel ids can be called — a paid slug throws before any request is made.