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

README.md

Language Markdown6.1 KB154 lines

mcp-server/README.md

  1. # Meridian Orders MCP server
  2. A **read-only** [Model Context Protocol](https://modelcontextprotocol.io) server
  3. over Plinth, Meridian Display Co.'s order system (both fictional; see the
  4. [root README](../README.md)). It is built to be **hosted on Cloudflare
  5. Workers** so the whole company can share one remote server, with a stdio entry
  6. for local use.
  7. > **Status:** all six read tools are implemented. Live requests are **gated
  8. > off** by default: nothing reaches production until
  9. > `PLINTH_ALLOW_LIVE_REQUESTS=true` and an API key are both set.
  10. ## Two runtimes, one core
  11. The tool set and the Plinth client are runtime-agnostic (Web-standard
  12. `fetch`/`URL` only, with config injected and no `process.env` in the core). Two
  13. thin entry points adapt it to each runtime:
  14. | Entry | Transport | Use |
  15. | --- | --- | --- |
  16. | `src/index.ts` | stdio | **Local**: exercise the tools from a desktop MCP client. |
  17. | `src/worker.ts` | Streamable HTTP + SSE | **Hosted (Cloudflare Workers)**: the company-wide deployment. |
  18. Both call the same `registerTools()` in `src/server.ts`, so behavior is
  19. identical across runtimes.
  20. ### Why Cloudflare Workers
  21. - One hosted URL the whole company connects to, with no per-person setup.
  22. - The Plinth client already uses only Web-standard APIs, so it runs on the V8
  23. isolate unchanged.
  24. - Sessions run as Durable Objects (`McpAgent`), which gives the shared rate
  25. limiter (10 requests / 10 s) a natural home, and later OAuth-based access
  26. control.
  27. ## Safety
  28. The Plinth API is **production-only (no sandbox)** and the key has **global
  29. read scope**. So:
  30. - only the six **read** methods are registered; the write methods are absent;
  31. - a four-layer failsafe (`src/lib/guard.ts`) makes a write impossible to
  32. dispatch, even through the passthrough tools;
  33. - `plinthRequest()` refuses to run unless `PLINTH_ALLOW_LIVE_REQUESTS=true`
  34. **and** a key are set.
  35. ## Layout
  36. ```
  37. src/
  38. ├── index.ts # Node/stdio entry (local)
  39. ├── worker.ts # Cloudflare Workers entry (McpAgent, remote HTTP)
  40. ├── env.ts # Workers env bindings (Env)
  41. ├── server.ts # registerTools() + createServer(), shared and runtime-agnostic
  42. ├── rateLimiterDO.ts # the shared rate-limiter Durable Object
  43. ├── tools/
  44. │ ├── index.ts # read-only tool registry
  45. │ ├── types.ts # ToolModule / ToolContext / ToolResult
  46. │ ├── getOrders.ts
  47. │ ├── getStatusCatalog.ts
  48. │ ├── getCustomers.ts
  49. │ ├── getSplitShipments.ts
  50. │ ├── getProduct.ts # unprobed: passthrough params
  51. │ └── getInventory.ts # unprobed: passthrough params
  52. └── lib/
  53. ├── config.ts # configFromEnv() (injected) + live-request gate
  54. ├── http.ts # buildUrl + plinthRequest(ctx, …), gated
  55. ├── guard.ts # the write-prevention failsafe
  56. ├── rateLimit.ts # reserveSlot() scheduler + in-memory limiter
  57. ├── errors.ts # Config / PlinthApi / RateLimit
  58. ├── collections.ts # [] vs "1".."N" normalization; numeric coercion
  59. ├── dates.ts # ISO -> yyyymmdd request format
  60. ├── orderNumber.ts # MD2639582 -> order_ref resolution
  61. └── classification.ts # sensitivity classes (not wired yet)
  62. verify/ # offline checks of the failsafe and the rate limiter
  63. test/ # node --test unit tests for collections, dates, orderNumber
  64. tsconfig.json # Node core + build (excludes worker: Node globals)
  65. tsconfig.worker.json # Worker + shared core (Workers globals)
  66. wrangler.jsonc # Cloudflare config (DO bindings, vars, compat flags)
  67. ```
  68. Node and Workers global types conflict, so each runtime has its own tsconfig
  69. over a shared `tsconfig.base.json`. The shared core typechecks under both.
  70. ## Tools (all read-only)
  71. | Tool | Plinth method | Notes |
  72. | --- | --- | --- |
  73. | `get_orders` | `orders.fetch` | order/batch lookup |
  74. | `get_status_catalog` | `statuses.list` | status catalog (lookup table) |
  75. | `get_customers` | `customers.fetch` | email / date range; **no phone filter** |
  76. | `get_split_shipments` | `orders.splitShipments` | bare-array response |
  77. | `get_product` | `products.fetch` | **unprobed**, passthrough params |
  78. | `get_inventory` | `inventory.fetch` | **unprobed**, passthrough params |
  79. The write methods `orders.setStatus` and `inventory.adjust` are **out of
  80. scope** and not implemented. Adding write capability needs explicit sign-off
  81. from the owner and whoever maintains Plinth.
  82. ## Develop
  83. ```bash
  84. cd mcp-server
  85. npm install
  86. npm run typecheck:all # node core + worker
  87. npm test # build, then the offline checks and unit tests below
  88. ```
  89. `npm test` builds `dist/` and runs `verify/failsafe.mjs` (the failsafe blocks
  90. every write path), `verify/ratelimit.mjs` (the limiter paces bursts) and the
  91. unit tests in `test/`. None of them touch the network. CI
  92. (`.github/workflows/ci.yml`) runs the typecheck, the tests and
  93. `wrangler deploy --dry-run` on every push.
  94. ### Local (stdio)
  95. ```bash
  96. npm run dev
  97. ```
  98. ### Local (Workers, via Wrangler)
  99. ```bash
  100. cp .dev.vars.example .dev.vars # fill in PLINTH_API_KEY locally (gitignored)
  101. npm run dev:worker # http://localhost:8787/mcp (needs Node 22+)
  102. ```
  103. ## Deploy to Cloudflare
  104. ```bash
  105. wrangler secret put PLINTH_API_KEY # never commit the key
  106. npm run deploy
  107. ```
  108. `wrangler.jsonc` declares the `PlinthMcp` and `RateLimiterDO` Durable Objects and
  109. the non-secret vars (`PLINTH_API_BASE_URL`, `PLINTH_ALLOW_LIVE_REQUESTS`). Check
  110. the build any time with:
  111. ```bash
  112. npx wrangler deploy --dry-run
  113. ```
  114. ## Next steps
  115. 1. **Company-wide access control.** `src/worker.ts` is unauthenticated. Front
  116. it with OAuth (`@cloudflare/workers-oauth-provider`) or Cloudflare Access,
  117. and thread the verified identity into `ToolContext` so
  118. `lib/classification.ts` can enforce per-customer scoping and PII redaction.
  119. The API provides none.
  120. 2. Probe `products.fetch` / `inventory.fetch` and replace their passthrough
  121. schemas.
  122. The API reference lives at [`../docs/PLINTH-API.md`](../docs/PLINTH-API.md).