mcp-customer-server
by willsu42
README.md
# mcp-customer-server
A runnable MCP server (TypeScript, official `@modelcontextprotocol/sdk`) exposing two tools:
- `get_customer_record(customer_id: "CUST-XXXXX")`
- `trigger_refund(customer_id: "CUST-XXXXX", amount: positive number, reason: string, min 10 chars)`
Built to satisfy three things explicitly: strict input validation, standards-compliant
JSON-RPC error mapping, and a hard guarantee that stdout carries nothing but protocol frames.
## Setup
```bash
npm install
npm run build
npm start # runs the server on stdio, waiting for JSON-RPC input
```
## Design decisions
**Low-level `Server` API, not the high-level `McpServer` helper.** The high-level API is
convenient but hides how validation failures get reported. Here, `CallToolRequestSchema`'s
handler validates arguments explicitly with Zod and throws `McpError(ErrorCode.InvalidParams, ...)`
on failure. The SDK's protocol layer turns that into a real JSON-RPC `error` object
(`code: -32602`) — not a "successful" tool result with an error message buried inside.
**Protocol errors vs. tool-execution errors are kept separate on purpose:**
- *Malformed input* (wrong `customer_id` format, negative amount, short reason) → thrown
as `McpError` → JSON-RPC `error` field, `-32602 InvalidParams`.
- *Valid input that fails for a business reason* (customer not found, account suspended,
refund exceeds balance) → returned as a normal tool result with `isError: true`. The
request was well-formed; the *outcome* was a failure. Collapsing these two cases into
one error path is a common MCP server mistake that breaks client-side retry logic.
- *Unexpected internal errors* → logged in full to stderr, but the client only ever sees a
generic `InternalError` — no stack traces or internals leak over the wire.
**stdout isolation is enforced, not just followed by convention.** `src/logger.ts` writes
structured JSON logs to `process.stderr` exclusively, and `lockdownConsole()` monkey-patches
`console.log`/`.info`/`.debug` at startup to redirect to stderr as well, so a stray debug
statement (yours or a dependency's) can't silently corrupt the JSON-RPC stream on stdout.
**Zod is the single source of truth for validation.** `zod-to-json-schema` generates the
`inputSchema` advertised in `tools/list` directly from the same Zod schemas used to validate
incoming calls, so the two can't drift apart.
## Verifying stdio isolation and protocol compliance
```bash
npm run test:stdio
```
This spawns the compiled server, drives it through a real `initialize` → `tools/list` →
`tools/call` handshake over raw stdio, and asserts:
- every line on stdout parses as JSON (fails loudly on the first non-JSON line)
- an out-of-format `customer_id` and an invalid `amount`/`reason` all come back as
`error.code === -32602`
- valid calls succeed with no `error` field
- log output was actually observed on stderr (proving separation, not just absence of failure)
## Manual exploration with MCP Inspector
```bash
npx @modelcontextprotocol/inspector node build/server.js
```
Try:
- `get_customer_record` with `customer_id: "CUST-00001"` → succeeds
- `get_customer_record` with `customer_id: "12345"` → JSON-RPC error, `-32602`
- `trigger_refund` with `amount: -10` → JSON-RPC error, `-32602`
- `trigger_refund` with `reason: "no"` → JSON-RPC error, `-32602` (under 10 chars)
- `trigger_refund` against `CUST-00003` (seeded as suspended) → tool result with
`isError: true`, not a protocol error
## Mock data
Three customers are seeded in `src/data.ts` for demo purposes: `CUST-00001` and
`CUST-00002` are active with balances; `CUST-00003` is suspended, to exercise the
business-logic error path.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues