helpdesk
Uses Cloudflare as the hosting platform: routing, Host and Origin checks, OAuth metadata endpoints, Durable Object storage for tickets and auth state, and post-teardown checks of Durable Objects in the Cloudflare dashboard.
The MCP server is deployed as an HTTP server on Cloudflare Workers, with ticket and auth data held in a SQLite-backed Durable Object; the README covers deploying with Wrangler, configuring secrets and allowed hosts, and cleaning up the Worker and its Durable Object namespaces.
Stores helpdesk data (tickets, customers, refunds) and OAuth state in SQLite, including a SQLite-backed Durable Object for remote deployments, backing tools like get_ticket, search_tickets, add_internal_note, issue_refund and export_tickets.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@helpdesksearch tickets for cus_2977 and summarize ticket 2331"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-from-zero
Companion code for the karnstack course MCP from Zero. It builds one
Model Context Protocol server, helpdesk, for a small company that sells
booking software to gyms and yoga studios. Each lesson's end state is a git
tag (lesson-02 to lesson-10); check out the tag for the lesson you are on.
The code follows MCP revision 2026-07-28.
git checkout lesson-10
npm ci
npm run check # typecheck, tests, and wire validationRequires Node.js 24.14.1 or newer. Node runs the TypeScript files directly (type stripping), so there is no build step.
What each tag adds
Tag | Lesson | Adds |
| JSON-RPC on the wire | hand-written stdio server: |
| Tools: list, call, and the model in between |
|
| Tools cost context |
|
| From hand-rolled to the SDK | the server rewritten on |
| Resources and prompts | the refund policy and ticket resources, the |
| Writes, confirmation and long calls |
|
| Going remote with Streamable HTTP |
|
| Who is calling: OAuth for MCP | our own small authorization server in the Worker (CIMD, PKCE, audience-bound tokens, step-up to |
| When the data fights back | ticket 2331, the 30-day refund rule, random user-bound export handles, |
Related MCP server: TicketDesk MCP
This tag: lesson-10, when the data fights back
Ticket 2331 is now in the helpdesk (attacks/ticket-2331.ts, loaded by
data/seed.ts): a bug report from customer cus_2977 whose text tells
any AI assistant to refund all twelve of the customer's $99 charges, and
any coding assistant to run a script. The server returns it as written;
that is correct. Two rules changed so the damage is bounded whatever the
model does:
The refund limit counts the customer, not the call. In
src/tools/issue_refund.ts,checkRefundLimit()asksdb.refundTotalCents(customerId, since)for the customer's refunds in the last 30 days. If this refund would take them above $100, it needsrefunds:write(only Dana's account), and over HTTP the answer is403 insufficient_scopewitherror_description="Refunds above $100 per customer in 30 days need refunds:write". Twelve refunds of $99 now stop after the first. A retry of a refund that already happened is not counted again.Export handles are random and bound to the user.
export_ticketsreturnsexp_plus 32 random hex digits, and stores the file as<user_id>:<handle>. The same handle read by anyone else is "Resource not found".
The attacks/ folder has runnable demos (see attacks/README.md): the
naive agent with approvals off, a poisoned and rug-pulling server, a
pinning defence, and the audience and Origin checks tried from outside.
npm run attack runs the naive agent.
Deploying and cleaning up work as in lesson 9 below. If you deployed
lesson 9, deploy again to get these rules, and when you are done, run the
same cleanup: rotate TOKEN_SIGNING_KEY, wrangler delete, check the
Durable Objects in the dashboard, remove the connector.
OAuth for MCP (from lesson 9)
The Worker now holds two separate parties on separate paths:
the MCP server, an OAuth resource server: it only checks tokens;
a small teaching authorization server (
src/auth-server/): it signs people in and issues tokens.
Path | Who answers | What it is |
| MCP server | needs |
| MCP server (the SDK's | protected resource metadata (RFC 9728): the resource and its authorization server |
| authorization server metadata (RFC 8414) | endpoints, PKCE S256, |
| authorization server | fetches the client's Client ID Metadata Document, shows a sign-in and consent page, redirects back with a one-time code and |
| authorization server | checks the code, client, redirect URI, resource and PKCE verifier, and returns a one-hour access token |
File | What it does |
| routing, Host and Origin checks, the metadata documents, and the bearer gate ( |
|
|
|
|
|
|
|
|
|
|
| the two teaching accounts, Mei and Dana |
|
|
|
|
|
|
Scopes: tickets:read, tickets:write (notes, and refunds up to $100),
refunds:write (refunds over $100). Mei may have the first two; Dana all
three. A client first asks for what the protected resource metadata lists,
tickets:read tickets:write. The tool list depends on the token: without
tickets:write, only the four read tools, so tools/list is cached
private: one list per token, never shared between callers. A refund
over $100 without refunds:write gets 403 with
scope="refunds:write" before the tool runs; the client signs in again
asking for it, and only Dana's sign-in grants it. The rule is in
src/tools/issue_refund.ts and holds over stdio too (as a tool error).
There are no refresh tokens: when the hour is up, the client signs in again. The billing provider is called with the helpdesk's own key, never with the user's token.
New seeded data for the step-up: ticket 2327 from Granite Peak Climbing
(cus_1310), an annual renewal of $1,188 (ch_8790) charged after they
cancelled.
The whole flow, recorded
scripts/oauth-flow.ts is a scripted client that does what Claude Code
does, using Claude Code's real Client ID Metadata Document
(https://claude.ai/oauth/claude-code-client-metadata) as its client_id:
node scripts/with-worker.ts node scripts/oauth-flow.ts /tmp/flowscripts/with-worker.ts starts wrangler dev with fresh secrets for that
run. To run the Worker yourself (npm run dev), copy .dev.vars.example
to .dev.vars and fill it in.
transcripts/lesson-09/ has the recordings: 01-mei-first-connection
(eight round trips from 401 to the tool list), 02-refund-step-up
(Mei refused, Dana steps up, confirms, refund re_5531) and
03-refused-tokens (wrong audience, expired, wrong key).
Deploy your own copy
npx wrangler login
npx wrangler deploy # first deploy, to learn your hostname
# put "helpdesk.<your-subdomain>.workers.dev" in ALLOWED_HOSTS in wrangler.jsonc
npx wrangler secret put TOKEN_SIGNING_KEY # for example: openssl rand -base64 32
npx wrangler secret put REQUEST_STATE_KEY # at least 32 bytes
npx wrangler secret put MEI_PASSWORD
npx wrangler secret put DANA_PASSWORD
npx wrangler secret put BILLING_API_KEY # any value; the billing provider is a stand-in
npx wrangler deployThe global_fetch_strictly_public compatibility flag in wrangler.jsonc
keeps the Client ID Metadata Document fetch on the public internet.
Connect:
Claude Code:
claude mcp add --transport http helpdesk https://helpdesk.<your-subdomain>.workers.dev/mcp, then/mcpto sign in. Claude Code uses its Client ID Metadata Document and a loopback redirect.claude.ai: Settings, Connectors, Add custom connector, the same URL, and "Use Claude's published identity". claude.ai uses Client ID Metadata Documents only when the metadata advertises both
client_id_metadata_document_supported: trueandnoneintoken_endpoint_auth_methods_supported, which ours does. Its redirect ishttps://claude.ai/api/mcp/auth_callback.
Clean up when you are done
claude mcp remove helpdesk # Claude Code
# claude.ai: Settings, Connectors, helpdesk, Remove
npx wrangler secret put TOKEN_SIGNING_KEY # a new value: every token issued so far stops working
npx wrangler delete # deletes the Worker
npx wrangler logout # optionalThen, in the Cloudflare dashboard (Workers and Pages, then Durable
Objects), check that no HelpdeskStore or AuthStore namespace is left,
and delete any that is. This tag uses no KV namespaces. Secrets belong to
the Worker and go with it.
Checks
npm run test:worker signs Mei and Dana in through the real flow and
checks the 401, the metadata, the token audience (a token for another
service is refused), the step-up, and the authorization server's refusals.
npm run conformance runs the official suite through a small proxy that
adds a test token (scripts/conformance.ts), since the suite cannot sign
in.
Streamable HTTP on Workers (from lesson 8)
The same server now also runs on Cloudflare Workers, over Streamable HTTP:
one endpoint, POST /mcp, one HTTP request per message. Nothing about the
tools changed. Because it has no authorization yet, the deployed server is
read-only: it registers only search_tickets, get_ticket,
get_customer and export_tickets, and serves only the seeded, invented
data.
File | What it does |
| the Worker: route |
|
|
| Worker name |
| types for the Worker's bindings ( |
| the official conformance scenarios this server is expected to fail, and why |
Run it locally
(From lesson 9 the endpoint needs a token, so this request now gets 401.)
npm run dev # wrangler dev: http://localhost:8787/mcp
curl -s http://localhost:8787/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"8"},"io.modelcontextprotocol/clientCapabilities":{}}}}'transcripts/lesson-08/*.http.md has every request and response from such
a session: server/discover, a tool call, a progress stream (SSE), 202
for a notification, the -32020, -32022 and -32602 errors, 405 for
GET, 403 for a foreign Origin or Host, and an older client's
initialize with no session.
Checks (from lesson 8)
npm run check now also starts the Worker locally (wrangler dev, fresh
state) for npm run test:worker (the official client against the Worker,
both protocol eras, the Host, Origin, 405, -32020, -32602 and 202
cases) and for npm run conformance, the official suite
(@modelcontextprotocol/conformance 0.2.0-alpha.12, --requirements 2026-07-28). Most of its scenarios call test fixtures an SDK registers,
which the helpdesk does not have; conformance/baseline.yml lists them.
Every message the server sent passed the suite's wire schema check.
Writes, confirmation and long calls (from lesson 7)
The server now has all six tools from the lesson 4 design.
add_internal_note (ticket_id, body) adds a note only staff see.
Annotations: not read-only, not destructive, not idempotent (each call adds
a note).
issue_refund (ticket_id, charge_id, amount_cents) refunds a
charge through the billing provider (src/billing.ts, a stand-in called
with the helpdesk's own API key). In order:
If a refund with the idempotency key
refund-<ticket>-<charge>exists, it returns that same refund. A duplicate call pays nothing.It checks the request against the database: the charge must belong to the ticket's customer, and the amount must fit what is left of the charge.
It asks the user. The first call returns
resultType: "input_required"with a form ("Refund $49.00 to Blue Heron Yoga for ticket 2318?") and arequestState. The client shows the form and calls again with a new id, the answer ininputResponses, and therequestStateechoed back.On a "yes", it calls the billing provider and records the refund:
re_5530for ticket 2318. That id is kept for ticket 2318; every other refund counts up fromre_5531.
The requestState is signed with HMAC (the SDK's
createRequestStateCodec), expires after 10 minutes, and is bound to the
method and the user. A forged one is rejected with -32602. A client that
cannot show a form gets -32021 (missing client capability) and no
refund. Older clients get the same question as a real elicitation/create
request; the SDK does that for us.
export_tickets (status) writes matching tickets to a CSV file,
500 per batch, slowly on purpose (EXPORT_DELAY_MS, 250 ms a batch). If
the request carries a progressToken it sends notifications/progress
after each batch. If the client cancels, it stops. It returns an export id
(exp_1), an explicit handle; read the file at helpdesk://exports/exp_1.
Environment variables for src/stdio.ts: BILLING_API_KEY (default: a
test key), HELPDESK_STATE_KEY (at least 32 bytes; default: random per
process), EXPORT_DELAY_MS, HELPDESK_USER, HELPDESK_DB.
The live tool list now measures 717 tokens, plus 49 for the instructions: 766 of the 800 budget.
Resources and prompts (from lesson 6)
Added in lesson 6:
Resources (
src/resources.ts,registerResources(server, db)):helpdesk://policies/refunds, the refund policy as Markdown;the template
helpdesk://tickets/{ticket_id}, one ticket as JSON.resources/listlists the 180 open tickets; any ticket can be read. An unknown ticket is error-32602("Resource not found").
A prompt (
src/prompts.ts,registerPrompts(server, db)):triage_ticketwith one argument,ticket_id. It embeds the ticket and the policy and asks for a triage. In Claude Code:/mcp__helpdesk__triage_ticket 2318.A resource link:
get_customerresults end with aresource_linkto the refund policy. The host decides whether to read it.Cursor pagination in
search_tickets: 50 tickets per page and an opaquenext_cursorto pass back ascursor. All 12,400 tickets take 248 pages. A cursor only works for the search that made it.Cache hints on the list results and on each resource (
ttlMs,cacheScope).
The SDK answers subscriptions/listen by itself; see
transcripts/lesson-06/subscriptions-listen.md.
Try it in Claude Code with @helpdesk:helpdesk://tickets/2318 (attach a
ticket) or /mcp__helpdesk__triage_ticket 2318 (run the prompt).
The server (from lesson 5)
The helpdesk server runs on the official TypeScript SDK v2,
@modelcontextprotocol/server 2.3.1. The data is a deterministic SQLite
database (Node's built-in node:sqlite): 3,100 customers and 12,400
tickets, 180 of them open, created on first run.
npm start # node src/stdio.ts: the server on stdio
npm run seed # recreate data/helpdesk.db from scratchNode prints an ExperimentalWarning for node:sqlite on stderr. It is
harmless; node --disable-warning=ExperimentalWarning src/stdio.ts hides it.
File | What it does |
|
|
| one file per tool, each exporting |
|
|
| the |
|
|
|
|
| the refund policy resource and the ticket template (lesson 6) |
| the |
| a stand-in for the billing provider's refund API (lesson 7) |
|
|
| the deterministic seed: ticket 2318 from Blue Heron Yoga ( |
The SDK server is dual-era: it answers server/discover from current
clients and initialize from older ones, with no code of ours.
Test it in the MCP Inspector
The Inspector speaks the older protocol unless you ask, so pass
--protocol-era auto:
npx @modelcontextprotocol/inspector@2.11.0 --protocol-era auto node src/stdio.ts # web UI
npx @modelcontextprotocol/inspector@2.11.0 --cli --protocol-era auto node src/stdio.ts --method tools/list
npx @modelcontextprotocol/inspector@2.11.0 --cli --protocol-era auto node src/stdio.ts \
--method tools/call --tool-name get_ticket --tool-arg 'ticket_id="2318"'Quote string arguments as JSON ('ticket_id="2318"'). The CLI parses
ticket_id=2318 as the number 2318, and the schema rejects it.
Connect Claude Code and VS Code
This folder's .mcp.json registers the server at project scope: open the
folder in Claude Code and approve it. Or add it yourself:
claude mcp add helpdesk -- node "$PWD/src/stdio.ts"Claude Code names the tools mcp__helpdesk__get_ticket and so on. For VS
Code, use the .vscode/mcp.json shown for lesson 3 with
src/stdio.ts in place of handwritten/server.ts.
Checks (from lesson 5)
npm run check adds the SDK tests (test/tools.test.ts runs the official
client against the server in-process) and measures the live server's tool
list against the 800 token budget (npm run budget). The SDK adds a
$schema key to every input schema, about 17 tokens per tool, so the live
list costs more than the lesson 4 design.
The hand-written server and the lesson 2 to 4 files stay as reference;
src/ does not import them.
What is in the repo
Path | What it is |
| the hand-written stdio server |
| the messages typed by hand in the lessons, one per line |
| checks every message in |
| the spec's JSON Schema for 2026-07-28 and 2025-11-25, copied unchanged |
| replays message files into a stdio server and records the real session |
| turns a recorded |
| a logging pipe between a real client and a stdio server |
| counts the tokens a server's tool list costs |
| saved tool lists of public servers |
| the six-tool design for our server |
| recorded sessions, as |
| the SDK server (lesson 5 on) |
| the deterministic helpdesk data |
| lesson 10's attack and defence demos |
| tests, run with |
Files named *.invalid.jsonl hold deliberate mistakes and must fail
validation. Files named *.broken.jsonl record a broken session on purpose.
Pinned versions
Package | Version |
typescript | 6.0.3 |
@types/node | 24.19.1 |
ajv | 8.20.0 |
ajv-formats | 3.0.1 |
js-tiktoken | 1.0.21 |
@modelcontextprotocol/server | 2.3.1 |
@modelcontextprotocol/client | 2.3.1 (tests) |
zod | 4.6.5 |
wrangler | 4.146.0 |
@modelcontextprotocol/conformance | 0.2.0-alpha.12 |
The schema files come from
modelcontextprotocol/modelcontextprotocol
at commit 86f6e0c675afea9a8f233e09b6faf566b126086c (schema/2026-07-28/schema.json
and schema/2025-11-25/schema.json), under that project's license.
License
MIT, see LICENSE. The files in wire/schema/ are copied from the MCP
specification repository and keep that repository's license.
Available Tools
6 toolsadd_internal_noteAdd internal noteA
Add an internal note to a ticket. Only staff see notes, never the customer. Each call adds a new note.
| Name | Required | Description | Default |
|---|---|---|---|
| body | Yes | ||
| ticket_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| note_id | Yes | |
| ticket_id | Yes | |
| created_at | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=false, idempotentHint=false, and destructiveHint=false. The description adds real context beyond those flags: the audience/visibility model ('Only staff see notes, never the customer') and the practical non-idempotency consequence ('Each call adds a new note'), warning the agent that repeat calls create duplicates. It does not detail auth/permissions or return behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short sentences, front-loaded with the core action, and each adds a distinct fact (action, visibility, non-idempotency). No filler or repetition, though it is minimal rather than richly structured.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be explained, and annotations carry the safety/mutation profile. For a simple two-parameter write tool the description is nearly sufficient, with the notable gap being parameter semantics given 0% schema coverage.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate, but it only implicitly suggests a ticket reference and note body without naming or explaining ticket_id or body. No format guidance (e.g., ticket_id is numeric-only) is provided, leaving the schema to carry parameters it does not document.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource: 'Add an internal note to a ticket.' This is clearly distinct from siblings like search_tickets, get_ticket, export_tickets, and issue_refund, which an agent can tell apart without opening schemas. It stops short of explicitly naming siblings, so 4 rather than 5.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The line 'Only staff see notes, never the customer' implies this is the channel for staff-only communication, which hints at when to use it. However, no explicit when/when-not conditions or alternatives (e.g., a customer-visible reply tool) are named, so guidance remains implied.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
export_ticketsExport ticketsARead-only
Export tickets to a CSV file. Slow for large exports; reports progress. Returns an export id for the file.
| Name | Required | Description | Default |
|---|---|---|---|
| status | No | Only tickets with this status |
Output Schema
| Name | Required | Description |
|---|---|---|
| uri | Yes | |
| rows | Yes | |
| export_id | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With readOnlyHint=true and openWorldHint=false already declared, the description still adds real value: it warns that large exports are slow, notes progress reporting, and discloses the asynchronous-ish 'export id' return. That is meaningful operational context the annotations do not carry. It stops short of saying how long 'slow' is or whether the id must be polled elsewhere.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three tight sentences, front-loaded with purpose, then performance, then return. No filler or repetition of the name/title.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The output schema presumably covers the return shape, so the description needn't explain it further. The performance warning and export-id mention round out the key behavioral facts. The main gap is routing: nothing tells the agent why to pick this over search_tickets for non-file use cases.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% for the single 'status' parameter (with an enum), so the schema already fully documents it. The description adds nothing about the parameter, which is acceptable at this coverage level but earns only the baseline.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description names a specific verb and resource plus the output format: 'Export tickets to a CSV file.' That is far more precise than the tautological title. It does not, however, distinguish itself from siblings like search_tickets, which an agent might reasonably reach for when it just wants ticket data.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is a useful performance caveat ('Slow for large exports'), which is the closest thing to guidance, but no statement of when to use this over search_tickets, nor any exclusions or prerequisites. The agent is left to infer that export is for bulk file retrieval rather than interactive querying.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_customerGet customerARead-only
Fetch one customer account (a gym or a yoga studio) with its plan, contact email and recent charges. Customer ids start with cus_, for example cus_1042.
| Name | Required | Description | Default |
|---|---|---|---|
| customer_id | Yes | The customer id, e.g. cus_1042 |
Output Schema
| Name | Required | Description |
|---|---|---|
| name | Yes | |
| plan | Yes | |
| Yes | ||
| charges | Yes | |
| customer_id | Yes | |
| monthly_price_cents | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and openWorldHint=false, so the safety profile is covered by structured data. The description adds useful context about the record's composition and the id convention, but does not mention auth requirements, error behavior, or whether the fetch fails hard on a bad id.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two short sentences, front-loaded with the action and return contents, followed by the id convention. No filler and nothing repeated that the schema covers more precisely.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
A single-parameter read tool with full schema coverage, annotations carrying the safety profile, and an output schema that owns return-value documentation. Nothing an agent needs to invoke it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% with a single parameter that already documents the pattern and example, so the description merely restates the cus_ convention. With the schema doing all the work, baseline 3 is correct; the description adds no syntax or edge-case detail beyond it.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (Fetch) and resource (one customer account) and clarifies the domain entity (a gym or yoga studio) plus what the record carries (plan, contact email, recent charges). There is no competing customer tool among the siblings, but the scope is unambiguous enough that an agent knows exactly what it gets.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by the tool name and the 'fetch one' framing, but there is no explicit when-to-use guidance, no statement of prerequisites, and no pointer to an alternative (e.g. how to find a customer id before calling). Adequate but with clear gaps.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_ticketGet ticketARead-only
Fetch one support ticket by its number, with subject, status, customer id and the customer's messages. Ticket numbers are digits only, for example 2318.
| Name | Required | Description | Default |
|---|---|---|---|
| ticket_id | Yes | Ticket number, digits only, e.g. 2318 |
Output Schema
| Name | Required | Description |
|---|---|---|
| status | Yes | |
| subject | Yes | |
| messages | Yes | |
| ticket_id | Yes | |
| customer_id | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and openWorldHint=false, so the safety profile is covered. The description adds the shape of the payload (subject, status, customer id, messages), which is useful context, but says nothing about behavior for a missing/invalid ticket or any access restrictions.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two tight sentences: the action and returned fields come first, and the identifier format constraint follows. No filler, no repetition of the title.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, the description needn't explain return values, yet it helpfully summarizes them; annotations cover read-only semantics and the lone parameter is fully specified. The only real gap is error/not-found behavior, which is minor for a simple getter.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and the single parameter is fully documented with a regex pattern and example. The description's 'digits only, for example 2318' merely repeats the schema, adding no new semantics; baseline 3 is correct.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Fetch one support ticket by its number') and enumerates the returned fields, so the agent knows exactly what it gets. It distinguishes itself from search_tickets only implicitly through 'one ... by its number' rather than naming the sibling, which keeps it out of 5 territory.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by the required identifier: call this when you already have a ticket number. There is no explicit when-to-use statement, no mention of the search_tickets alternative for cases where you only have a name or subject, and no exclusion guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
issue_refundIssue refundADestructiveIdempotent
Refund a customer's charge for a ticket through the billing provider, after the user confirms. Safe to retry: one ticket and charge are refunded at most once. Amounts are in cents.
| Name | Required | Description | Default |
|---|---|---|---|
| charge_id | Yes | From get_customer, e.g. ch_8812 | |
| ticket_id | Yes | ||
| amount_cents | Yes | e.g. 4900 for $49.00 |
Output Schema
| Name | Required | Description |
|---|---|---|
| refund_id | Yes | |
| amount_cents | Yes | |
| idempotency_key | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=false, destructiveHint=true, idempotentHint=true, and openWorldHint=true, so the safety profile is covered structurally. The description nonetheless adds two pieces of real context: the confirmation requirement and the at-most-once refund guarantee per ticket/charge pair, which reinforces (and does not contradict) idempotentHint.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short sentences, front-loaded with the action, followed by the idempotency guarantee and the unit convention. Every sentence carries information, though "through the billing provider" is closer to implementation detail than decision-relevant content.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be explained, and annotations carry the mutation/destructiveness profile. The description covers the action, precondition, retry semantics, and amount units; only failure modes (e.g. already-refunded or invalid charge) are left unaddressed.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 67% — charge_id and amount_cents are documented in the schema, ticket_id is not. The description's "Amounts are in cents" restates the unit already conveyed by amount_cents' example (4900 for $49.00), so it adds little beyond the schema. Baseline 3 is appropriate given the schema does most of the work.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (refund) and resource (a customer's charge for a ticket), and clarifies the path taken (through the billing provider). No sibling tool overlaps with refunding, so there is no ambiguity to resolve against search_tickets, get_ticket, get_customer, export_tickets, or add_internal_note.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
"After the user confirms" gives a clear precondition for invocation, which is exactly the kind of guidance an agent needs before calling an irreversible-feeling money operation. It stops short of naming any alternative or when-not-to-use condition, so it does not reach a full 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_ticketsSearch ticketsARead-only
Search support tickets by words in the subject or messages, 50 per page, most recently updated first. Returns ticket number, subject, status and customer id for every ticket it finds.
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Words to match; leave out to list all tickets | |
| cursor | No | next_cursor from the previous page, to get the next page | |
| status | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| total | Yes | How many tickets match in all |
| tickets | Yes | |
| next_cursor | No | Pass back as cursor for the next page; absent on the last page |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations only declare readOnlyHint and openWorldHint, so the description's disclosure of page size (50 per page), sort order (most recently updated first), and pagination dependency (cursor from previous page) adds real operational context. Returned fields are enumerated but overlap with the existing output schema, so not a full 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two tight sentences with the search scope and pagination front-loaded and nothing extraneous. The trailing enumeration of returned fields is largely recoverable from the output schema, a small redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, the description needn't explain return values, and it still adds pagination, ordering, and match scope. The main gap is the absence of when-to-use routing against the other four ticket siblings, plus no mention of the status filter.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 67%; the description clarifies what 'query' matches against (subject or messages), which adds meaning beyond the schema's terse 'Words to match.' It says nothing about 'status' filtering or 'cursor' usage, so it only partially compensates for the coverage gap.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Clear verb+resource ('Search support tickets') with the matching scope spelled out (words in subject or messages), which distinguishes it from the single-ticket sibling get_ticket. It does not name any sibling explicitly, so it stops short of a 5.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description says how the search behaves but never says when to choose it over get_ticket, export_tickets, or an unfiltered list. The closest thing to guidance ('leave out to list all tickets') lives in the schema, not the description, leaving usage to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
6 tool updates
v1.0.0- First observed
add_internal_note - First observed
export_tickets - First observed
get_customer - First observed
get_ticket - First observed
issue_refund - First observed
search_tickets
TDQS
Scored across 6 tools
Each tool targets a distinct resource and action: search vs. get for tickets, get for customers, export, note addition, and refund. Boundaries are clear, and the ticket lookup pair is well differentiated (bulk search vs. single-ticket detail).
All names follow a consistent verb_noun snake_case pattern (search_tickets, get_ticket, get_customer, export_tickets, add_internal_note, issue_refund). No mixed conventions or vague verbs.
Six tools is well-scoped for a helpdesk surface, with each tool clearly earning its place. No redundancy or thin coverage.
Read and write operations cover lookup, export, notes, and refunds, but there is no ticket creation, status update/close, or customer-facing reply, which are core helpdesk lifecycle actions. These gaps will limit end-to-end agent workflows.
Maintenance
Related MCP Connectors
Helpdesk tickets from your AI: find, reply, take, hold, complete — within your own role. OAuth.
Read tickets, users, orgs, macros and satisfaction ratings; create, update and comment on tickets.
Read tickets, contacts, companies, agents and groups; create, update and reply to tickets.
OAuth 2.1 short-link tools for AI agents with scoped tokens, approvals, audit logs, and revocation.
Related MCP Servers
AlicenseAqualityBmaintenanceEnables managing support tickets from Claude, Cursor, and other AI tools, including listing, creating, updating, and replying to tickets.68 npmMIT- AlicenseNot gradedqualityCmaintenanceEnables AI assistants to search and retrieve support tickets, add notes, close tickets, and access SLA policies through MCP tools, resources, and prompts over Streamable HTTP.MIT
- FlicenseNot gradedqualityCmaintenanceEnables SaaS support desk operations with sixteen tools for customer lookup, invoice and ticket access, refunds, and admin actions, organized into four risk tiers.-
- FlicenseNot gradedqualityCmaintenanceEnables AI clients to access and manage an internal support ticket queue through MCP tools, resources, and prompts, including searching and viewing tickets, adding comments, closing tickets with confirmation, reading knowledge base articles, and viewing queue summaries over OAuth-secured Streamable HTTP.-