Skip to main content
Glama
willsu42
by willsu42

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

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

npm run test:stdio

This spawns the compiled server, drives it through a real initializetools/listtools/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

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.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/willsu42/mcp-customer-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server