README.md
Language Markdown6.1 KB154 lines
# Meridian Orders MCP serverA **read-only** [Model Context Protocol](https://modelcontextprotocol.io) serverover Plinth, Meridian Display Co.'s order system (both fictional; see the[root README](../README.md)). It is built to be **hosted on CloudflareWorkers** so the whole company can share one remote server, with a stdio entryfor 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 coreThe tool set and the Plinth client are runtime-agnostic (Web-standard`fetch`/`URL` only, with config injected and no `process.env` in the core). Twothin 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 isidentical 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 V8isolate unchanged.- Sessions run as Durable Objects (`McpAgent`), which gives the shared ratelimiter (10 requests / 10 s) a natural home, and later OAuth-based accesscontrol.## SafetyThe Plinth API is **production-only (no sandbox)** and the key has **globalread 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 todispatch, 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 limitertest/ # node --test unit tests for collections, dates, orderNumbertsconfig.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 tsconfigover 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 ofscope** and not implemented. Adding write capability needs explicit sign-offfrom the owner and whoever maintains Plinth.## Develop```bashcd mcp-servernpm installnpm run typecheck:all # node core + workernpm test # build, then the offline checks and unit tests below````npm test` builds `dist/` and runs `verify/failsafe.mjs` (the failsafe blocksevery write path), `verify/ratelimit.mjs` (the limiter paces bursts) and theunit 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)```bashnpm run dev```### Local (Workers, via Wrangler)```bashcp .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```bashwrangler secret put PLINTH_API_KEY # never commit the keynpm run deploy````wrangler.jsonc` declares the `PlinthMcp` and `RateLimiterDO` Durable Objects andthe non-secret vars (`PLINTH_API_BASE_URL`, `PLINTH_ALLOW_LIVE_REQUESTS`). Checkthe build any time with:```bashnpx wrangler deploy --dry-run```## Next steps1. **Company-wide access control.** `src/worker.ts` is unauthenticated. Frontit 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 passthroughschemas.The API reference lives at [`../docs/PLINTH-API.md`](../docs/PLINTH-API.md).