MCP Starter
A production-grade MCP server for order management, featuring tools for looking up and refunding orders with safety measures like confirmation tokens, rate limiting, and error handling.
README
MCP Starter
A production-grade Model Context Protocol server you can fork for client work. Both transports are implemented and tested: stdio (local) and Streamable HTTP (remote, stateless).
54 tests, clean typecheck, no any in the source.
npm install
npm test # 54 passing
npm run build
npm start # stdio
npm run start:http
npm run inspect # official MCP Inspector UI
What MCP is
MCP is a standard way to give an AI assistant access to somebody else's system. Before it, every integration was one-off: custom code for Shopify, more for HubSpot, more for the client's internal database. MCP replaces that with one protocol, so any assistant that speaks it — Claude Desktop, Claude Code, Cursor, ChatGPT — can use any MCP server.
A server exposes three things:
| Concept | What it is | In this repo |
|---|---|---|
| Tools | Actions the model can take | lookup_order, refund_order |
| Resources | Read-only data pulled into context | the return policy, an order record |
| Prompts | Reusable templates the user picks | draft_reply |
Tools are what clients pay for. Resources and prompts are polish.
Transport is how the assistant reaches the server. stdio: the host launches the server as a local process and talks over stdin/stdout — used for anything touching a local machine, zero hosting cost. Streamable HTTP: the server lives at a URL — used for multi-user or SaaS-backed work, costs hosting but the customer installs nothing. Same server code, different entrypoint.
What makes this production grade
Most MCP examples online are a server.tool() call and a console.log. Here is the difference.
Destructive actions are gated by a signed, single-use token
refund_order moves money. Calling it without a confirmToken does not refund — it returns a summary plus a token. The model must show the summary to the user and call again with the token.
The token is an HMAC bound to the exact arguments, so the model cannot get approval for a $1 refund and then execute a $189 one. It is single-use, so a retry loop can't fire twice, and it expires in five minutes. All four properties are tested, including the escalation attempt.
Errors never leak internals to the model
Every handler is wrapped. Domain errors become a clean isError tool result the model can recover from; unknown throws become a generic message while the real one goes to the logs. A test asserts a Postgres connection string in an exception never reaches the model-visible text.
Errors are also classified retryable vs not, and the retry logic respects it — a 404 fails once instead of burning the full retry budget.
Upstream calls have deadlines, retries, and jitter
Hard per-attempt timeout (a hung vendor API would otherwise hang the whole assistant turn), exponential backoff with full jitter, Retry-After honored, caller cancellation respected without retrying. Tested with an injected fetch and clock, so the suite is fast and deterministic.
Auth is constant-time; the server refuses to run open in production
Tokens are SHA-256 hashed before comparison, so timingSafeEqual gets equal-length buffers and comparison time leaks nothing. With NODE_ENV=production and no tokens configured, the server returns 500 rather than serving an open endpoint.
DNS-rebinding protection that actually works
A local MCP server is reachable from any web page the user visits: the page resolves an attacker domain to 127.0.0.1 and POSTs to it. Checking the Host header blocks this.
The SDK ships this feature, but it does an exact string compare against the raw header — which includes the port — so an allowlist of 127.0.0.1 silently 403s every request on any non-default port. Our integration test caught it. host-guard.ts compares hostname and port separately: an entry without a port matches any port, an entry with one must match both. IPv6 literals and case-insensitivity are handled and tested.
Everything else
- Structured tool output (
outputSchema+structuredContent) so hosts get typed data, not a string to re-parse - Tool annotations (
readOnlyHint,destructiveHint,idempotentHint) so hosts can prompt appropriately - Per-credential rate limiting (token bucket, lazy refill, swept to bound memory)
- Single-flight TTL cache — a model firing the same tool three times in a turn makes one upstream call
- Structured NDJSON logging to stderr with request IDs, child loggers, and automatic secret redaction
- Config validated at boot — bad config kills the process with a readable message
- Graceful shutdown — SIGTERM drains in-flight requests with a hard-stop backstop
- Multi-stage Dockerfile, non-root, with a healthcheck
- CI across Node 20/22/24 plus
npm audit
Layout
src/
config.ts zod-validated env
logger.ts NDJSON to stderr, redaction
errors.ts error taxonomy, public vs internal messages
http-client.ts timeouts, retries, jitter
cache.ts TTL + single-flight
rate-limit.ts token bucket
confirm.ts HMAC confirmation tokens
factory.ts dependency graph + server assembly
domain/orders.ts upstream adapter (HTTP + in-memory)
tools/
wrap.ts timing, logging, error normalization
orders.ts the tools themselves
transports/
stdio.ts local entrypoint
http.ts remote entrypoint
host-guard.ts DNS-rebinding protection
test/
unit.test.ts 37 tests
integration.test.ts 17 tests, real MCP client + real HTTP server
Wire it into Claude Desktop
~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"starter": {
"command": "node",
"args": ["/absolute/path/to/mcp-starter/dist/transports/stdio.js"]
}
}
}
Restart Claude Desktop.
Deploy the HTTP version
docker build -t mcp-starter .
docker run -p 3000:3000 \
-e NODE_ENV=production \
-e MCP_AUTH_TOKENS="$(openssl rand -hex 32)" \
-e ALLOWED_HOSTS=your-domain.com \
mcp-starter
Or push to GitHub and point Railway at it with start command npm run start:http. Set MCP_AUTH_TOKENS and ALLOWED_HOSTS. See .env.example for every setting.
Building a client's server
- Replace
HttpOrderServiceinsrc/domain/orders.tswith their API. Parse responses through a zod schema so an upstream change surfaces as one validation error instead of malformed data reaching the model. - Rewrite the tools around what their staff does by hand every day. One tool per job to be done, not per API endpoint. Five well-named tools beat twenty.
- Write descriptions that say when to call the tool. That single line drives most of the accuracy.
- Mark every destructive tool
destructiveHint: trueand route it throughConfirmationGate. - Keep
InMemoryOrderServiceworking. It's how the tests stay fast and how you demo without credentials.
Gotchas that bite people
- Never write to stdout in a stdio server. One
console.logcorrupts the protocol stream. Everything here logs to stderr. - Don't return raw API dumps. Every token of tool output is context the model must read.
- Stateless HTTP scales; sessions don't. This creates a fresh server per request, so it runs on any number of instances with no sticky routing. If you need session state, switch
sessionIdGeneratortorandomUUID— but then you own session eviction.
Scoping this as paid work
- Wrap one internal API for Claude Desktop — stdio, no hosting, clean one-time deliverable.
- Hosted multi-tenant server with auth — HTTP, needs hosting, key rotation, and upkeep when the upstream API changes. Never sell this as a fixed one-time build; attach a retainer.
- Fix or finish a half-built MCP server — fits the Fix & Finish line.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.