Skip to main content
Glama

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 validation

Requires 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

lesson-02

JSON-RPC on the wire

hand-written stdio server: server/discover, errors, initialize for older clients

lesson-03

Tools: list, call, and the model in between

tools/list and tools/call for get_ticket, tool errors vs protocol errors, wire/tap.ts

lesson-04

Tools cost context

tools/tool-cost.ts, measured public tool lists, the six-tool design with an 800 token budget

lesson-05

From hand-rolled to the SDK

the server rewritten on @modelcontextprotocol/server with the three read tools, SQLite data, stdio entry

lesson-06

Resources and prompts

the refund policy and ticket resources, the triage_ticket prompt, cursor pagination in search_tickets

lesson-07

Writes, confirmation and long calls

add_internal_note, issue_refund (confirmation, signed requestState, idempotency key), export_tickets (progress, cancellation, export handle)

lesson-08

Going remote with Streamable HTTP

src/worker.ts on Cloudflare Workers, data in a SQLite Durable Object, read-only deploy, conformance suite

lesson-09

Who is calling: OAuth for MCP

our own small authorization server in the Worker (CIMD, PKCE, audience-bound tokens, step-up to refunds:write), the bearer gate, write tools for signed-in staff

lesson-10

When the data fights back

ticket 2331, the 30-day refund rule, random user-bound export handles, attacks/ demos

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() asks db.refundTotalCents(customerId, since) for the customer's refunds in the last 30 days. If this refund would take them above $100, it needs refunds:write (only Dana's account), and over HTTP the answer is 403 insufficient_scope with error_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_tickets returns exp_ 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

POST /mcp

MCP server

needs Authorization: Bearer <token>; without one, 401 with WWW-Authenticate: Bearer ... resource_metadata="<origin>/.well-known/oauth-protected-resource/mcp"

GET /.well-known/oauth-protected-resource/mcp

MCP server (the SDK's oauthMetadataResponse)

protected resource metadata (RFC 9728): the resource and its authorization server

GET /.well-known/oauth-authorization-server

authorization server metadata (RFC 8414)

endpoints, PKCE S256, client_id_metadata_document_supported, token_endpoint_auth_methods_supported: ["none"], authorization_response_iss_parameter_supported

GET, POST /authorize

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 iss

POST /token

authorization server

checks the code, client, redirect URI, resource and PKCE verifier, and returns a one-hour access token

File

What it does

src/worker.ts

routing, Host and Origin checks, the metadata documents, and the bearer gate (requireBearerAuth with expectedResource); verifyAccessToken() turns our JWT into the SDK's AuthInfo

src/auth-server/metadata.ts

authServerMetadata(origin) and SCOPES

src/auth-server/cimd.ts

fetchClientMetadata(clientId) (HTTPS only, no IP addresses, no redirects, 5 KB, 5 s) and redirectAllowed() (exact, or any port on loopback)

src/auth-server/authorize.ts

authorize(): the consent page and the code

src/auth-server/token.ts

token(): code for token

src/auth-server/tokens.ts

signAccessToken() and readAccessToken(): HS256 JWTs with iss, sub, aud, scope, client_id, exp

src/auth-server/accounts.ts

the two teaching accounts, Mei and Dana

src/auth-server/store.ts

AuthStore, a SQLite Durable Object; takeCode() makes a code single-use

src/step-up.ts

StepUpRequired and stepUpChallenge(): a tool's rule becomes a 403 insufficient_scope

src/caller.ts

callerOf(ctx): the user and scopes, from the token over HTTP

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/flow

scripts/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 deploy

The 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 /mcp to 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: true and none in token_endpoint_auth_methods_supported, which ours does. Its redirect is https://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                        # optional

Then, 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

src/worker.ts

the Worker: route /mcp only, check Host and Origin (DNS rebinding), then the SDK's createMcpHandler, which builds a fresh server per request

src/db-durable.ts

HelpdeskStore, a SQLite-backed Durable Object holding the helpdesk database (seeded on first use), and durableSql(), the same Sql interface over RPC

wrangler.jsonc

Worker name helpdesk, the Durable Object binding HELPDESK, and the vars ALLOWED_HOSTS, ALLOWED_ORIGINS, HELPDESK_MODE

src/env.d.ts, worker-configuration.d.ts

types for the Worker's bindings (npm run types regenerates the second)

conformance/baseline.yml

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:

  1. If a refund with the idempotency key refund-<ticket>-<charge> exists, it returns that same refund. A duplicate call pays nothing.

  2. 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.

  3. 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 a requestState. The client shows the form and calls again with a new id, the answer in inputResponses, and the requestState echoed back.

  4. On a "yes", it calls the billing provider and records the refund: re_5530 for ticket 2318. That id is kept for ticket 2318; every other refund counts up from re_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/list lists 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_ticket with 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_customer results end with a resource_link to the refund policy. The host decides whether to read it.

  • Cursor pagination in search_tickets: 50 tickets per page and an opaque next_cursor to pass back as cursor. 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 scratch

Node prints an ExperimentalWarning for node:sqlite on stderr. It is harmless; node --disable-warning=ExperimentalWarning src/stdio.ts hides it.

File

What it does

src/server.ts

createServer(db): a factory that builds an McpServer with the instructions, cache hints and tools

src/tools/<tool_name>.ts

one file per tool, each exporting register<ToolName>(server, db)

src/result.ts

toolResult(data) (text + structuredContent) and toolError(text)

src/db.ts

the Helpdesk queries over a small Sql interface, and the schema

src/db-node.ts

openNodeDb(): the Sql interface on node:sqlite, seeding on first use

src/stdio.ts

serveStdio(() => createServer(db))

src/resources.ts

the refund policy resource and the ticket template (lesson 6)

src/prompts.ts

the triage_ticket prompt (lesson 6)

src/billing.ts

a stand-in for the billing provider's refund API (lesson 7)

src/caller.ts

callerOf(ctx): who is calling (lesson 7: the local user)

data/seed.ts

the deterministic seed: ticket 2318 from Blue Heron Yoga (cus_1042, charges ch_8811 and ch_8812), ticket 2331 from cus_2977

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

handwritten/server.ts

the hand-written stdio server

wire/*.jsonl

the messages typed by hand in the lessons, one per line

wire/validate.ts

checks every message in wire/ and transcripts/ against the official schema

wire/schema/

the spec's JSON Schema for 2026-07-28 and 2025-11-25, copied unchanged

scripts/capture.ts

replays message files into a stdio server and records the real session

scripts/render.ts

turns a recorded .jsonl session into a readable .md

wire/tap.ts

a logging pipe between a real client and a stdio server

tools/tool-cost.ts

counts the tokens a server's tool list costs

tools/measured/

saved tool lists of public servers

tools/helpdesk-design.json

the six-tool design for our server

transcripts/lesson-NN/

recorded sessions, as .jsonl (raw lines) and .md (readable)

src/

the SDK server (lesson 5 on)

data/seed.ts

the deterministic helpdesk data

attacks/

lesson 10's attack and defence demos

test/

tests, run with node --test

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 tools
add_internal_noteAdd internal noteA

Add an internal note to a ticket. Only staff see notes, never the customer. Each call adds a new note.

ParametersJSON Schema
NameRequiredDescriptionDefault
bodyYes
ticket_idYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
note_idYes
ticket_idYes
created_atYes

TDQS

A3.5/5.0
Behavior4/5

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.

Conciseness4/5

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.

Completeness4/5

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.

Parameters2/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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 ticketsA
Read-only

Export tickets to a CSV file. Slow for large exports; reports progress. Returns an export id for the file.

ParametersJSON Schema
NameRequiredDescriptionDefault
statusNoOnly tickets with this status

Output Schema

ParametersJSON Schema
NameRequiredDescription
uriYes
rowsYes
export_idYes

TDQS

A3.6/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines2/5

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 customerA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
customer_idYesThe customer id, e.g. cus_1042

Output Schema

ParametersJSON Schema
NameRequiredDescription
nameYes
planYes
emailYes
chargesYes
customer_idYes
monthly_price_centsYes

TDQS

A3.9/5.0
Behavior3/5

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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines3/5

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 ticketA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
ticket_idYesTicket number, digits only, e.g. 2318

Output Schema

ParametersJSON Schema
NameRequiredDescription
statusYes
subjectYes
messagesYes
ticket_idYes
customer_idYes

TDQS

A3.6/5.0
Behavior3/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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 refundA
DestructiveIdempotent

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
charge_idYesFrom get_customer, e.g. ch_8812
ticket_idYes
amount_centsYese.g. 4900 for $49.00

Output Schema

ParametersJSON Schema
NameRequiredDescription
refund_idYes
amount_centsYes
idempotency_keyYes

TDQS

A4.1/5.0
Behavior4/5

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.

Conciseness4/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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 ticketsA
Read-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.

ParametersJSON Schema
NameRequiredDescriptionDefault
queryNoWords to match; leave out to list all tickets
cursorNonext_cursor from the previous page, to get the next page
statusNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
totalYesHow many tickets match in all
ticketsYes
next_cursorNoPass back as cursor for the next page; absent on the last page

TDQS

A3.5/5.0
Behavior4/5

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.

Conciseness4/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines2/5

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.

  1. 6 tool updatesv1.0.0
    • First observedadd_internal_note
    • First observedexport_tickets
    • First observedget_customer
    • First observedget_ticket
    • First observedissue_refund
    • First observedsearch_tickets

TDQS

A3.9/5.0

Scored across 6 tools

Disambiguation5/5

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).

Naming Consistency5/5

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.

Tool Count5/5

Six tools is well-scoped for a helpdesk surface, with each tool clearly earning its place. No redundancy or thin coverage.

Completeness3/5

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

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables 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
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables SaaS support desk operations with sixteen tools for customer lookup, invoice and ticket access, refunds, and admin actions, organized into four risk tiers.
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables 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.
    -