Repository avatar
Developer Tools
v1.0.1
active

mcp-reference-server

io.github.markusvankempen/mcp-reference-server

Reference MCP server to check, validate and test clients and SEPs. 22 tools, stdio and HTTP.

Documentation

MCP Reference Server — A Full-Stack MCP Demo

npm MCP Protocol SEPs Tools Node.js License

A reference MCP server for the Model Context Protocol. Use it to check, validate and test your client, your server, and proposed protocol changes (SEPs, Specification Enhancement Proposals) against a running implementation. Run it in under five minutes.

  • Reference: 22 tools, 3 resources, 5 prompts, three transports (stdio, SSE, Streamable HTTP), three auth modes, API keys, per-tool gates, rate limiting, and a desk for health, tools, admin, and the log.
  • Validate: /health says the server is alive, /test says it works, and the automated suites exercise tools, auth, events, tasks and skills.
  • Test SEPs: 15 specifications and SEP prototypes (groups, skills, tasks, events and more) sit behind live switches. Flip one and watch how your client copes. See the SEP control plane.
MCP server (mcp-reference-server 1.0.0)server/
MCP specification and SEPsmodelcontextprotocol.io · SEP process

Personal open-source project. SEP behavior here is a prototype for testing, not the specification; the specification at modelcontextprotocol.io is authoritative.


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:

FeatureWhat you learn
22 tools with intent-named descriptionsNouns 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 promptsUser-facing prompts vs agent-facing tools
Tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint: false)Client confirm/retry UX
Server instructionsThe README the model actually reads
isError: true on every failureClients don't need to parse ok: false
Attribution scar — create_ticket without requester_email201 is not done. The bot owns the ticket.
Schema discovery — get_schema → run_queryOne query tool beats a pile of query_* names
0 tools discovered — hand a laptop path to a cloud runnerThe silent failure with no error and no warning
stdio + SSE + Streamable HTTP from one codebaseThree transports, same 22 tools
Auth modes (off / write / all) + per-tool gate + per-tool auth lockSecurity is an operator concern
tools/list_changed broadcast when admin flips a gateSSE and Streamable HTTP sessions refresh; one-shot curl does not
Rate limiting with retry_after_seconds in the errorStop the model retrying in a loop
PII redaction on run_query schema=customersSame tool, same endpoint; the phone field is REDACTED without the pii scope
/health vs /testAlive ≠ works. Public /test is read-only; cwd stays off public /health
Live observability — /log page with counters, error log, call traceSee 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 offThe control plane is part of the server, not a side door
Three ways to operate one server — /admin Remote config and the mcp-reference-server command lineSame steps, same Demo setup, checked against each other
Groups (mcp-reference-server 1.0.0)Prototype of io.modelcontextprotocol/groups. A client that negotiates it gets describe_server and get_schema from tools/list, then groups/list and groups/get. Seven groups: query, tickets-write, admin, events, traffic, jobs, client. A client that does not negotiate still gets all 22 tools. server/README.md
Skills (SEP-2640)io.modelcontextprotocol/skills. A skill is a workflow and files (skills/list, skills/get, resources/read), not a tool and not a scope. Built in: work-an-open-ticket, open-a-ticket, read-the-queue. Admins upload more. server/README.md
Tasks (SEP-2663)io.modelcontextprotocol/tasks. create_ticket and run_test_job can return a flat task handle. Poll tasks/get, or open tasks/stream (SEP-2694, draft) and resume with after. After ttlMs, tasks/get is -32602 with data.status expired until the retain window ends. tasks/result is not implemented. server/README.md
Events (SEP-3415 draft)io.modelcontextprotocol/events. events/list and events/poll read the same bus as GET /sse. events/stream is a short push. Webhook is not offered. server/README.md
Sampling, roots, and progressdraft_reply calls sampling/createMessage and does not post the draft. list_workspace_roots calls roots/list and does not read the files. An open run_test_job sends notifications/progress when the request carries a progressToken. A one-shot call has no client to ask. server/README.md
Live SEP control plane (/admin#adm-seps)Toggle 15 MCP specifications & SEPs live (2026-07-28 §2.1, SEP-1686, SEP-2640, SEP-2575, SEP-2567, SEP-2127, SEP-3415, SEP-3313, SEP-19, SEP-2636, SEP-1577, SEP-1036, SEP-1850, SEP-2549, SEP-2164) to test client resilience and audit compliance. server/README.md lists what each switch changes

Quickstart — five minutes

# Run directly from npm — nothing to clone or install
npx mcp-reference-server           # stdio (IDE spawns this)
MCP_MODE=http npx mcp-reference-server  # HTTP — opens /health /test /admin /mcp

Then open in a browser:

URLWhat it shows
http://127.0.0.1:8787/healthIs the process alive? (cwd only on localhost)
http://127.0.0.1:8787/testRead-only smoke: seed tickets, schemas, and run_query
http://127.0.0.1:8787/adminAuth mode, API keys, tool gates, observability
http://127.0.0.1:8787/logCall counters, error log, full call trace
http://127.0.0.1:8787/toolsEvery tool, with its scope, a Try button, and the curl that calls it. Tabs for groups, skills, tasks, and the test job
http://127.0.0.1:8787/help#help-specsReference for the groups, skills, tasks, and events extensions

From a terminal

The same mcp-reference-server 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-reference-server status                                    # http://127.0.0.1:8787/mcp by default
npx mcp-reference-server call run_query schema=tickets 'filter={"status":"open"}' limit=5   # any tool, values typed from its schema
npx mcp-reference-server demo apply --user demo --password demo    # users, keys, auth write, 30 calls / 60 s
npx mcp-reference-server demo check --user demo --password demo    # acts as each role and as nobody
npx mcp-reference-server 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.

/health — liveness check with version, tool count, and cwd

/test — read-only smoke test: seed tickets, schemas, and run_query

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-reference-server": {
      "type": "stdio",
      "command": "npx",
      "args": ["mcp-reference-server"],
      "env": { "MCP_MODE": "stdio" }
    }
  }
}

Bob / Cursor / Windsurf — .bob/mcp.json / .cursor/mcp.json / .windsurf/mcp.json:

{
  "mcpServers": {
    "mcp-reference-server": {
      "command": "npx",
      "args": ["mcp-reference-server"],
      "env": { "MCP_MODE": "stdio" }
    }
  }
}

Per-client detail is in docs/LOCAL.md and docs/BOB.md, and a running desk serves copy-paste pages for each client at /clients/.


Tools

The server has 22 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, SEPs, transports, events, gates, locks and scopes), set_mqtt, users (create_user, delete_user), API keys (issue_api_key, revoke_api_key), import_settings and sample traffic (generate_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. run_test_job waits a set time and ends as a chosen task status, to test long-running calls and SEP-2663 tasks (server/README.md). On an open call it also sends notifications/progress when the request carries a progressToken. draft_reply asks the connected client's model for a draft and does not post it. list_workspace_roots returns the workspace roots the client chose to share and does not read the files (server/README.md). 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)

ToolScopeWhen to useLesson
describe_serverreadFirst call, and after any denialDiscovery beats guessing
create_ticketwriteOpen a ticket — always pass requester_emailOmit it → 201 + bot owns the ticket
add_commentwriteComment on a known ticketWrite tool; gated
close_ticketwriteResolve a ticket, optionally add resolution notedestructiveHint: true, idempotentHint: true
get_schemareadBefore any queryNo name lists every name with its kind (data, query, tool); a name gives that shape or those arguments
run_queryreadThe 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.

URIWhat it returnsSame gate as
ticket://TCK-1001One ticket by idrun_query
tickets://openLive open ticket list (top 25)run_query
schema://ticketsQuery 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.

PromptLesson it teaches
search-open-ticketsList open tickets and who owns each one
attribution-scarCreate a ticket with no requester email. The service account owns it
schema-discoveryget_schema (no name) → get_schema (name) → run_query walkthrough
close-ticket-flowadd_comment then close_ticket in sequence
diagnose-serverdescribe_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-reference-server/
  server/       MCP server — tools, resources, prompts, auth, HTTP pages
    src/cli/    The command line client (mcp-reference-server <command>)
    clients/    Copy-paste setup pages served at /clients/ (editors, browser, CLI, settings)
  docs/         Walkthroughs: demo, local, admin and security, Bob
  Dockerfile    node:20-bookworm-slim, non-root USER node, WORKDIR /app

Docs

DocWhat's in it
docs/README.mdIndex. Server mcp-reference-server 1.0.0
docs/DEMO.mdEnd-to-end demo guide — 16 live curl demos, every lesson, real captured output
docs/LOCAL.mdLocal stdio + HTTP walkthrough
docs/ADMIN-AND-SECURITY.mdAuth modes, API keys, tool gates, SEP switches
docs/BOB.mdIBM Bob specific setup
server/README.mdFull reference: the 22 tools, groups, skills, tasks, sampling, roots, progress, every endpoint curl, and the command line

Environment variables

VariableDefaultWhat it does
MCP_MODEstdiostdio or http
PORT8080HTTP only. npm run http uses 8787
HOST127.0.0.10.0.0.0 inside container (set automatically)
AUTH_MODEoffoff · write · all
ADMIN_USER / ADMIN_PASSWORDdemo / demo/admin login. Default disabled on a public bind until ADMIN_PASSWORD is set
CORS_ORIGINSunsetExtra Origin values allowed on /mcp. Localhost and same-host are always allowed
API_KEY / API_KEY_SCOPESunsetRegister one key at boot
MCP_API_KEYunsetstdio credential. The command line client sends it as a bearer key
MCP_USERNAME / MCP_PASSWORDunsetstdio basic auth. The command line client sends them as HTTP Basic
MCP_URLhttp://127.0.0.1:8787/mcpCommand line client only: the server to talk to
MCP_SKILLS_DIR~/.mcp-reference-server/skillsUploaded skills. They survive a restart. Tickets do not
MCP_TASK_TTL_MS60000How long a task handle lives
MCP_USERSunset"alice:secret:read,write" extra logins
RATE_LIMIT / RATE_LIMIT_WINDOW_MS60 / 60000Calls per window per caller
TENANT_IDunsetIf 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 (+ Secure on HTTPS).
  • /health being green does not mean the ticket went to the right person.
  • Public /test does not create or close tickets. Use /test?write=1 after admin sign-in.
  • Remote config 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 · github.com/markusvankempen · markusvankempen.github.io · npm