io.github.markusvankempen/mcp-ticket-demo
MCP server demo — tickets, schema discovery, auth, and PII gating. stdio + HTTP.
Install
Add this to your client's MCP configuration (Claude Desktop: claude_desktop_config.json; Cursor: .cursor/mcp.json; VS Code and others use the same "mcpServers" shape).
mcp-ticket-democlaude mcp add mcp-ticket-demo -e 'MCP_API_KEY=<your mcp_api_key>' -e 'AUTH_MODE=<AUTH_MODE>' -e 'MCP_USERS=<your mcp_users>' -e 'RATE_LIMIT=<RATE_LIMIT>' -- npx -y mcp-ticket-demo@1.9.0
{
"mcpServers": {
"mcp-ticket-demo": {
"command": "npx",
"args": [
"-y",
"mcp-ticket-demo@1.9.0"
],
"env": {
"MCP_API_KEY": "<your mcp_api_key>",
"AUTH_MODE": "<AUTH_MODE>",
"MCP_USERS": "<your mcp_users>",
"RATE_LIMIT": "<RATE_LIMIT>"
}
}
}
}
Environment variables
MCP_API_KEYsecret API key for authenticated tool calls. Issue one on /admin → API Keys. Required when AUTH_MODE=write or AUTH_MODE=all.AUTH_MODEAuthentication mode: off (default), write (write+pii tools gated), all (every tool gated).MCP_USERSsecret Extra username/password users: 'alice:secret:read,write'. Comma-separated, colon-delimited.RATE_LIMITMax tool calls per window per caller. Default 60.
Documentation
From the project's README on GitHub, refreshed weekly. Links open on GitHub.
Contents
MCP Ticket Demo — A Full-Stack MCP Reference Server
A production-shaped MCP server you can run in under five minutes. Ships with 19 tools, 3 resources, 5 prompts, three auth modes, API key management, per-tool gates, rate limiting, a live observability dashboard, and a VS Code / Bob / Cursor / Windsurf control-plane extension.
Use it to learn MCP, test IDE integrations, run live demos, or as a reference implementation when building your own server.
Companion to **MCP as a Platform: What I Learned Building a Portfolio of MCP Servers** · MCP Dev Summit Toronto · 5 October 2026 · **Talk slides →**
| MCP server (3.0.1) | npm install mcp-ticket-demo |
| VS Code extension (1.16.0) | VS Code Marketplace · Open VSX |
| Talk slides | markusvankempen.github.io/linuxfoundation-mcp-dev-summit |
Personal open-source project.
What makes this useful beyond a hello-world
Most MCP examples stop at "here is a tool that returns a string." This one goes further:
| Feature | What you learn |
|---|---|
| 19 tools with intent-named descriptions | Nouns live in a schema, verbs stay as tools — not request(path, method) |
3 resources (ticket://, tickets://open, schema://) | resources/list enumerates instances; reads use the same gate() as the matching tool |
| 5 MCP prompts | User-facing prompts vs agent-facing tools |
Tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint: false) | Client confirm/retry UX |
| Server instructions | The README the model actually reads |
isError: true on every failure | Clients don't need to parse ok: false |
Attribution scar — create_ticket without requester_email | 201 is not done. The bot owns the ticket. |
Schema discovery — get_schema → run_query | One query tool beats a pile of query_* names |
| 0 tools discovered — hand a laptop path to a cloud runner | The silent failure with no error and no warning |
| stdio + SSE + Streamable HTTP from one codebase | Three transports, same 19 tools |
Auth modes (off / write / all) + per-tool gate + per-tool auth lock | Security is an operator concern |
tools/list_changed broadcast when admin flips a gate | SSE and Streamable HTTP sessions refresh; one-shot curl does not |
Rate limiting with retry_after_seconds in the error | Stop the model retrying in a loop |
PII redaction on run_query schema=customers | Same tool, same endpoint; the phone field is REDACTED without the pii scope |
/health vs /test | Alive ≠ works. Public /test is read-only; cwd stays off public /health |
Live observability — /log page with counters, error log, call trace | See what the model is actually doing |
Operate it over MCP — settings (one update_settings patch), users, API keys and settings import are tools, settings export is run_query schema=settings, and most need the admin scope even when auth is off | The control plane is part of the server, not a side door |
Three ways to operate one server — /admin Remote config, the extension's Remote config tab, and the mcp-ticket-demo command line | Same steps, same Demo setup, checked against each other |
Quickstart — five minutes
# Run directly from npm — no clone needed
npx mcp-ticket-demo # stdio (IDE spawns this)
MCP_MODE=http npx mcp-ticket-demo # HTTP — opens /health /test /admin /mcp
# Or clone and run
git clone https://github.com/markusvankempen/mcp-ticket-demo
cd mcp-ticket-demo/server && npm install && npm run http
Then open in a browser:
| URL | What it shows |
|---|---|
| http://127.0.0.1:8787/health | Is the process alive? (cwd only on localhost) |
| http://127.0.0.1:8787/test | Read-only smoke: seed tickets, schemas, and run_query |
| http://127.0.0.1:8787/admin | Auth mode, API keys, tool gates, observability |
| http://127.0.0.1:8787/log | Call counters, error log, full call trace |
| http://127.0.0.1:8787/tools | Tool inventory with scope and auth status |
From a terminal
The same mcp-ticket-demo command is also a client for a running server, local or deployed. With no arguments it is still the MCP server, so the IDE configs below keep working.
npx mcp-ticket-demo status # http://127.0.0.1:8787/mcp by default
npx mcp-ticket-demo call run_query schema=tickets 'filter={"status":"open"}' limit=5 # any tool, values typed from its schema
npx mcp-ticket-demo demo apply --user demo --password demo # users, keys, auth write, 30 calls / 60 s
npx mcp-ticket-demo demo check --user demo --password demo # acts as each role and as nobody
npx mcp-ticket-demo status --url https://my-host.example.com # a deployed server
--json prints one document for scripts, and exit codes are 0 done · 1 refused · 2 usage · 3 unreachable. Commands that delete or reconfigure ask first. Every command, option and example is in server/README.md, and a copy-paste page is at /clients/cli.html on a running desk. demo apply changes real settings, so use it on a server you own.
Laptop login: demo / demo. On a public bind (HOST=0.0.0.0, container, Code Engine) set ADMIN_PASSWORD — the default is disabled. Write smoke is /test?write=1 after admin sign-in.
Add to your IDE (one line)
VS Code / GitHub Copilot — .vscode/mcp.json:
{
"servers": {
"mcp-ticket-demo": {
"type": "stdio",
"command": "npx",
"args": ["mcp-ticket-demo"],
"env": { "MCP_MODE": "stdio" }
}
}
}
Bob / Cursor / Windsurf — .bob/mcp.json / .cursor/mcp.json / .windsurf/mcp.json:
{
"mcpServers": {
"mcp-ticket-demo": {
"command": "npx",
"args": ["mcp-ticket-demo"],
"env": { "MCP_MODE": "stdio" }
}
}
}
Or install the extension and click Register server with all IDEs — it writes all four configs at once: VS Code Marketplace · Open VSX
Tools
The server has 19 tools. Six are the ticket desk, below. Eight operate the server itself: update_settings (every setting is one patch: auth mode, rate limit, call trace, desk sign-in, transports, events, gates, locks and scopes), set_mqtt, users, API keys, import_settings and sample traffic. Most of those need the admin scope even while auth is off; server/README.md lists the scope of each. Three are timers, start_timer, stop_timer and push_timer, with no get_timer (push_timer also streams the timer as events; see Events): you read a timer through the schema, run_query with schema=timers (server/README.md). push_log sends a log line as an event, and watch_system publishes the machine as system.info, including a container's cgroup limit. Plain lists are not tools of their own: run_query reads settings (the saved settings, importable as they are), system (the machine), events (the events already sent), and, with an admin credential, users, api_keys, trace, errors and counters. The page at /clients/dashboard.html draws them. Each of these tools is declared once and everything else is generated from that definition.
The ticket desk (6)
| Tool | Scope | When to use | Lesson |
|---|---|---|---|
describe_server | read | First call, and after any denial | Discovery beats guessing |
create_ticket | write | Open a ticket — always pass requester_email | Omit it → 201 + bot owns the ticket |
add_comment | write | Comment on a known ticket | Write tool; gated |
close_ticket | write | Resolve a ticket, optionally add resolution note | destructiveHint: true, idempotentHint: true |
get_schema | read | Before any query | No name lists every name with its kind (data, query, tool); a name gives that shape or those arguments |
run_query | read | The one query tool: schema=tickets (find by status / requester_email / query, or one ticket by id with body and comments), schema=customers (phone is REDACTED without the pii scope), plus assets, timers, system, settings, events (and, for an admin, users, api_keys, trace, errors, counters) | Nouns in a schema, verbs as tools. Replaces query_tickets / query_assets / … |
3 Resources
Resources are addressable and pinnable — clients can subscribe and refresh. Tools are for agent loops.
| URI | What it returns | Same gate as |
|---|---|---|
ticket://TCK-1001 | One ticket by id | run_query |
tickets://open | Live open ticket list (top 25) | run_query |
schema://tickets | Query schema shape (also customers, assets) | get_schema |
Call resources/list to browse — you do not need to know a URI ahead of time. A denied read is a JSON-RPC error (not a fake ticket document you can pin).
5 Prompts
User-facing prompts — the user picks these, the model executes them.
| Prompt | Lesson it teaches |
|---|---|
search-open-tickets | Find service-account scars in the live data |
attribution-scar | Create without requester_email → explain what broke |
schema-discovery | get_schema (no name) → get_schema (name) → run_query walkthrough |
close-ticket-flow | add_comment then close_ticket in sequence |
diagnose-server | describe_server — auth mode, scopes, available tools |
Auth model
off All tools open. No credential needed. Default for local dev.
write Read tools open. Write tools need a credential. The customer phone shows only with a `pii` credential.
all Every tool call requires a credential.
Credentials: Authorization: Bearer <api key> over HTTP · MCP_API_KEY env var over stdio.
Issue keys, set modes, toggle per-tool gates, and lock individual tools on /admin.
A change broadcasts notifications/tools/list_changed to connected SSE and Streamable HTTP sessions (Cursor/Bob after initialize). One-shot POST /mcp (curl) has no session — it sees the new list on the next call.
Repo layout
mcp-ticket-demo/
server/ MCP server — tools, resources, prompts, auth, HTTP pages
src/cli/ The command line client (mcp-ticket-demo <command>)
clients/ Copy-paste setup pages served at /clients/ (editors, browser, CLI, settings)
extension/ VS Code / Bob / Cursor / Windsurf control-plane extension (packs a copy of server/)
docs/ Walkthroughs, lessons learned, publishing guide
Dockerfile node:20-bookworm-slim, non-root USER node, WORKDIR /app
Docs
| Doc | What's in it |
|---|---|
| docs/README.md | Index of these guides, for server 3.0.1 and extension 1.16.0 |
| docs/DEMO.md | End-to-end demo guide — 16 live curl demos, every lesson, real captured output |
| docs/LESSONS-LEARNED.md | 15 lessons building a real MCP server — war stories + pre-publish checklist |
| docs/LOCAL.md | Local stdio + HTTP walkthrough |
| docs/REMOTE.md | Code Engine deploy + the 0-tools-discovered repro |
| docs/ADMIN-AND-SECURITY.md | Auth modes, API keys, tool gates |
| docs/BOB.md | IBM Bob specific setup |
| docs/PUBLISHING.md | npm + MCP Registry + VS Code Marketplace publish steps |
| docs/EXTENSION.md | The VS Code / Cursor / Bob extension — tabs, Remote config, commands |
| server/README.md | Full curl reference for every endpoint, and the command line client |
Environment variables
| Variable | Default | What it does |
|---|---|---|
MCP_MODE | stdio | stdio or http |
PORT | 8080 | HTTP only. npm run http uses 8787 |
HOST | 127.0.0.1 | 0.0.0.0 inside container (set automatically) |
AUTH_MODE | off | off · write · all |
ADMIN_USER / ADMIN_PASSWORD | demo / demo | /admin login. Default disabled on a public bind until ADMIN_PASSWORD is set |
CORS_ORIGINS | unset | Extra Origin values allowed on /mcp. Localhost and same-host are always allowed |
API_KEY / API_KEY_SCOPES | unset | Register one key at boot |
MCP_API_KEY | unset | stdio credential. The command line client sends it as a bearer key |
MCP_USERNAME / MCP_PASSWORD | unset | stdio basic auth. The command line client sends them as HTTP Basic |
MCP_URL | http://127.0.0.1:8787/mcp | Command line client only: the server to talk to |
MCP_USERS | unset | "alice:secret:read,write" extra logins |
RATE_LIMIT / RATE_LIMIT_WINDOW_MS | 60 / 60000 | Calls per window per caller |
TENANT_ID | unset | If set, writes need x-tenant-id header |
Honest limits
- Tickets live in memory — a new container starts from seed data.
- Admin auth is a session cookie, not SSO. Cookie is
HttpOnly(+Secureon HTTPS). /healthbeing green does not mean the ticket went to the right person.- Public
/testdoes not create or close tickets. Use/test?write=1after admin sign-in. - Remote config, the extension's Remote config tab and the command line change real settings (auth mode, users, keys, rate limit) on whatever server you point them at. Demo setup is for a server you own.
Author: Markus van Kempen · markus.van.kempen@gmail.com · markusvankempen.github.io · MCP Dev Summit Toronto talk · Talk slides · npm · VS Code Marketplace · Open VSX
Frequently asked questions
What is io.github.markusvankempen/mcp-ticket-demo?
MCP server demo — tickets, schema discovery, auth, and PII gating. stdio + HTTP.
How do I install io.github.markusvankempen/mcp-ticket-demo?
It runs on your machine. In Claude Code: claude mcp add mcp-ticket-demo -e 'MCP_API_KEY=<your mcp_api_key>' -e 'AUTH_MODE=<AUTH_MODE>' -e 'MCP_USERS=<your mcp_users>' -e 'RATE_LIMIT=<RATE_LIMIT>' -- npx -y mcp-ticket-demo@1.9.0. For other clients, paste the JSON configuration from this page into their MCP settings.
Does io.github.markusvankempen/mcp-ticket-demo need an API key?
Yes. It expects MCP_API_KEY, MCP_USERS, which you set in its configuration.
Is io.github.markusvankempen/mcp-ticket-demo open source?
Its code is public on GitHub, but the repository does not declare a standard license.
Who maintains io.github.markusvankempen/mcp-ticket-demo?
It is published by markusvankempen. AI.tn lists it but does not maintain or endorse it.
From the official MCP Registry. Listing is not an endorsement: review a server's code and permissions before giving it access to your data.