5.7 KiB
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
# 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 loads .env, builds ctx = { pages, invertedIndex, glossary, wikiPath } from the wiki folder via 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/ and 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 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.
ctxis 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.errorfor 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). It handles only the subset present in the wiki: scalar
key: valueandkey:+- itemarrays. Don't introduce frontmatter shapes it can't parse without upgrading the parser.
Deploy target
TrueNAS Scale custom app via 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.