# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this is Read-only MCP server (Streamable HTTP transport) that serves a compiled EasyWMS / Mecalux WMS wiki. Designed to be paired in a Claude client with a separate `wms-api` MCP that queries live WMS data: - **wms-wiki** (this repo) — *how the WMS is supposed to work* (concepts, modules, configuration). 5 tools + 2 resources, all read-only. - **wms-api** (separate repo) — *what the WMS currently contains* (live data, logs, workflows). The wiki itself is ~135 markdown pages under `wiki/`, with YAML frontmatter (title/type/tags/related/sources). It's regenerated upstream by a separate `mcp-wiki` compilation project and dropped in. ## Commands ```bash # Local dev — MCP_ALLOWED_HOSTS is required, the transport rejects all hosts otherwise. MCP_ALLOWED_HOSTS="127.0.0.1:3000,localhost:3000" HOST=127.0.0.1 PORT=3000 \ npm start # Smoke test (in another terminal): healthz + initialize + tools/list npm run smoke-test # Override target: MCP_URL=https://mcp-wms.arthur-ria.fr HOST_HEADER=mcp-wms.arthur-ria.fr npm run smoke-test # MCP Inspector for interactive debugging (still uses stdio under the hood) npm run inspect # Docker docker build -t mcp-wms-wiki:dev . docker run --rm -p 3000:3000 -e MCP_ALLOWED_HOSTS=127.0.0.1:3000 mcp-wms-wiki:dev # No test suite. Verification is the smoke-test script + tools/call against a live server. ``` ## Required env vars | Var | Default | Notes | |---|---|---| | `WIKI_PATH` | `./wiki` | Absolute or relative to project root | | `PORT` | `3000` | | | `HOST` | `0.0.0.0` | `127.0.0.1` for local dev to avoid LAN exposure | | `MCP_ALLOWED_HOSTS` | *(empty)* | **Required**. Comma-separated Host headers. Empty → DNS-rebind guard rejects everything | `.env.example` documents local-dev values. Production injects vars via the Docker / TrueNAS app definition — `.env` is for local dev only and is git-ignored. ## Architecture — the non-obvious parts **Startup (once)**: [src/index.js](src/index.js) loads `.env`, builds `ctx = { pages, invertedIndex, glossary, wikiPath }` from the wiki folder via [src/services/wiki-index.js](src/services/wiki-index.js), freezes it. Everything reads from this in-memory snapshot — no file watching, no rebuild at runtime. Restart to pick up wiki changes. **Per-request** (HTTP): each `/mcp` hit instantiates a *new* `StreamableHTTPServerTransport` AND a *new* `McpServer` via `createServer(ctx)`. The factory re-registers all 5 tools + 2 resources, capturing `ctx` by reference (cheap — no copies). This is required for safe stateless concurrency: the SDK pairs one `McpServer` to one transport, so sharing across concurrent requests would crosstalk responses. `res.on('close')` tears both down. **Transport mode**: stateless (`sessionIdGenerator: undefined`) + `enableJsonResponse: true` — JSON one-shot responses, no SSE long-polling. This is the simplification that makes the reverse-proxy story (NPM + Cloudflare) trivial. If a future tool needs streaming or server-initiated notifications, this assumption breaks and you'll need stateful mode. **DNS-rebind protection** is on by default and depends entirely on `MCP_ALLOWED_HOSTS`. Forgetting to set it = HTTP 403 on every request. The error is silent at startup. **Auth (V1 = passthrough)**: `authMiddleware` is mounted only on `/mcp`, never on `/healthz`. V2 OAuth hook point is documented inline in `src/index.js` — wire in `@modelcontextprotocol/sdk/server/auth/*` there, don't sprinkle auth elsewhere. **Tools and resources** live under [src/tools/](src/tools/) and [src/resources/](src/resources/). Each tool exports `ToolDefinition` (name + description + JSON-schema for docs) and `handle(args, ctx)`. The McpServer wiring in `createServer()` uses Zod schemas separately because the SDK's `server.tool()` takes Zod, not JSON-schema. Keep both in sync. **Path-traversal guard** is centralized in [src/services/page.js](src/services/page.js) `safeResolveWikiPath()`. The `get_wiki_page` tool layer is intentionally dumb — it forwards the raw client input. Do *not* add a second sanitization upstream: it gives false confidence and the previous attempt (a `.replace(/\.\.\//g, '')`) was bypassable. Any new file-reading code path must go through `safeResolveWikiPath`. ## Conventions / things not to do - **Don't add per-tool state.** `ctx` is the only mutable-looking surface (it's frozen). Tools take `(args, ctx)` and return `{ content: [{ type: 'text', text }] }`. Stick to this shape. - **Don't change the 5 tool names or input shapes** without coordinating with clients. They are the published contract: `search_wiki`, `get_wiki_page`, `list_wiki_sections`, `get_glossary_term`, `get_related_pages`. - **Don't write to disk at runtime.** This server is read-only by design. No caches that survive a restart, no logs to files (stderr only — Docker collects it). - **`console.error` for all logging.** stdout was reserved by the old stdio transport; the convention stays because the MCP Inspector flow (`npm run inspect`) still uses stdio. - **YAML frontmatter parsing is regex-based**, not a real YAML parser ([src/services/wiki-index.js](src/services/wiki-index.js)). It handles only the subset present in the wiki: scalar `key: value` and `key:` + ` - item` arrays. Don't introduce frontmatter shapes it can't parse without upgrading the parser. ## Deploy target TrueNAS Scale custom app via [compose.yaml](compose.yaml), behind Nginx Proxy Manager + Cloudflare on `mcp-wms.arthur-ria.fr`. The wiki is **baked into the image** (not a volume) — rebuild + redeploy on wiki updates. NPM needs `proxy_buffering off; proxy_read_timeout 3600;` even though we're on JSON one-shot, for safety. Cloudflare: DNS-only (gray cloud) recommended initially.