mattrguerra/meridian-orders-mcpmainPrivate on GitHub, published here in full

mcp-server/

Meridian Orders MCP server

A read-only Model Context Protocol server over Plinth, Meridian Display Co.'s order system (both fictional; see the root README). It is built to be hosted on Cloudflare Workers so the whole company can share one remote server, with a stdio entry for local use.

Status: all six read tools are implemented. Live requests are gated off by default: nothing reaches production until PLINTH_ALLOW_LIVE_REQUESTS=true and an API key are both set.

Two runtimes, one core

The tool set and the Plinth client are runtime-agnostic (Web-standard fetch/URL only, with config injected and no process.env in the core). Two thin entry points adapt it to each runtime:

Entry Transport Use
src/index.ts stdio Local: exercise the tools from a desktop MCP client.
src/worker.ts Streamable HTTP + SSE Hosted (Cloudflare Workers): the company-wide deployment.

Both call the same registerTools() in src/server.ts, so behavior is identical across runtimes.

Why Cloudflare Workers

  • One hosted URL the whole company connects to, with no per-person setup.
  • The Plinth client already uses only Web-standard APIs, so it runs on the V8 isolate unchanged.
  • Sessions run as Durable Objects (McpAgent), which gives the shared rate limiter (10 requests / 10 s) a natural home, and later OAuth-based access control.

Safety

The Plinth API is production-only (no sandbox) and the key has global read scope. So:

  • only the six read methods are registered; the write methods are absent;
  • a four-layer failsafe (src/lib/guard.ts) makes a write impossible to dispatch, even through the passthrough tools;
  • plinthRequest() refuses to run unless PLINTH_ALLOW_LIVE_REQUESTS=true and a key are set.

Layout

src/
├── index.ts                 # Node/stdio entry (local)
├── worker.ts                # Cloudflare Workers entry (McpAgent, remote HTTP)
├── env.ts                   # Workers env bindings (Env)
├── server.ts                # registerTools() + createServer(), shared and runtime-agnostic
├── rateLimiterDO.ts         # the shared rate-limiter Durable Object
├── tools/
│   ├── index.ts             # read-only tool registry
│   ├── types.ts             # ToolModule / ToolContext / ToolResult
│   ├── getOrders.ts
│   ├── getStatusCatalog.ts
│   ├── getCustomers.ts
│   ├── getSplitShipments.ts
│   ├── getProduct.ts        # unprobed: passthrough params
│   └── getInventory.ts      # unprobed: passthrough params
└── lib/
    ├── config.ts            # configFromEnv() (injected) + live-request gate
    ├── http.ts              # buildUrl + plinthRequest(ctx, …), gated
    ├── guard.ts             # the write-prevention failsafe
    ├── rateLimit.ts         # reserveSlot() scheduler + in-memory limiter
    ├── errors.ts            # Config / PlinthApi / RateLimit
    ├── collections.ts       # [] vs "1".."N" normalization; numeric coercion
    ├── dates.ts             # ISO -> yyyymmdd request format
    ├── orderNumber.ts       # MD2639582 -> order_ref resolution
    └── classification.ts    # sensitivity classes (not wired yet)

verify/                # offline checks of the failsafe and the rate limiter
test/                  # node --test unit tests for collections, dates, orderNumber
tsconfig.json          # Node core + build (excludes worker: Node globals)
tsconfig.worker.json   # Worker + shared core (Workers globals)
wrangler.jsonc         # Cloudflare config (DO bindings, vars, compat flags)

Node and Workers global types conflict, so each runtime has its own tsconfig over a shared tsconfig.base.json. The shared core typechecks under both.

Tools (all read-only)

Tool Plinth method Notes
get_orders orders.fetch order/batch lookup
get_status_catalog statuses.list status catalog (lookup table)
get_customers customers.fetch email / date range; no phone filter
get_split_shipments orders.splitShipments bare-array response
get_product products.fetch unprobed, passthrough params
get_inventory inventory.fetch unprobed, passthrough params

The write methods orders.setStatus and inventory.adjust are out of scope and not implemented. Adding write capability needs explicit sign-off from the owner and whoever maintains Plinth.

Develop

cd mcp-server
npm install
npm run typecheck:all      # node core + worker
npm test                   # build, then the offline checks and unit tests below

npm test builds dist/ and runs verify/failsafe.mjs (the failsafe blocks every write path), verify/ratelimit.mjs (the limiter paces bursts) and the unit tests in test/. None of them touch the network. CI (.github/workflows/ci.yml) runs the typecheck, the tests and wrangler deploy --dry-run on every push.

Local (stdio)

npm run dev

Local (Workers, via Wrangler)

cp .dev.vars.example .dev.vars   # fill in PLINTH_API_KEY locally (gitignored)
npm run dev:worker               # http://localhost:8787/mcp (needs Node 22+)

Deploy to Cloudflare

wrangler secret put PLINTH_API_KEY   # never commit the key
npm run deploy

wrangler.jsonc declares the PlinthMcp and RateLimiterDO Durable Objects and the non-secret vars (PLINTH_API_BASE_URL, PLINTH_ALLOW_LIVE_REQUESTS). Check the build any time with:

npx wrangler deploy --dry-run

Next steps

  1. Company-wide access control. src/worker.ts is unauthenticated. Front it with OAuth (@cloudflare/workers-oauth-provider) or Cloudflare Access, and thread the verified identity into ToolContext so lib/classification.ts can enforce per-customer scoping and PII redaction. The API provides none.
  2. Probe products.fetch / inventory.fetch and replace their passthrough schemas.

The API reference lives at ../docs/PLINTH-API.md.