Files
mcp-wms-wiki/CLAUDE.md
T

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. 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). 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, 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.