Files
mcp-wms-wiki/CLAUDE.md
T

74 lines
5.7 KiB
Markdown

# 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 `<name>ToolDefinition` (name + description + JSON-schema for docs) and `handle<Name>(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.