unraid-code-mode-mcp

unraid-code-mode-mcp

A code-mode MCP server for the Unraid 7.2+ GraphQL API that exposes search and execute tools, allowing LLM agents to introspect and call any GraphQL field via sandboxed JavaScript.

Category
访问服务器

README

unraid-code-mode-mcp

CI License: MIT Status: beta Version: v0.1.0-beta.3

A code-mode MCP server for the Unraid 7.2+ GraphQL API. Exposes two MCP tools — search and execute — that let an LLM agent introspect and call any Unraid GraphQL field by writing JavaScript that runs inside a sandboxed QuickJS WASM context.

Built as the GraphQL-flavoured sibling of unifi-code-mode-mcp and fortimanager-code-mode-mcp. Same architecture, same sandbox model, adapted to GraphQL introspection instead of OpenAPI.

Project status

This is a public beta. Install from source. Not on npm yet.

The server boots, both tools work, and the test suite is green (56/56 unit + integration tests across spec loader, dispatcher, sandbox, HTTP client, multi-tenant context, and server transports). A standalone npm run test:sandbox script exercises the QuickJS sync + Promise-callback host bridge with 25 sequential awaits, a 10-way Promise.all, mixed sequential/parallel patterns, and error propagation — these are the patterns LLMs actually emit, and they are the regression bar for the bridge.

Verified live against a single real Unraid 7.2 box (the maintainer's homelab) via scripts/mcp-call.mjs driving the stdio transport directly: info, array, shares, vms, docker, and online reads succeed; the VM SHUTOFF → RUNNING → SHUTOFF cycle via vmStart / vmStop mutations succeeds; sequential awaits and Promise.all both work end-to-end with real GraphQL latency; and the bundled SDL fallback path (introspection disabled) returns a human-readable diagnostic with a remediation hint instead of an opaque HTTP 400. The bundled SDL is pinned to a tagged unraid/api release (currently v4.33.0, no main-drift) and the introspection-disabled fallback is exercised by unit tests.

End-to-end LLM-mediated invocation is verified through two clients against the same Unraid 7.2 box:

Client Model Status Transcript
cursor-agent v2026.05.05 Claude Sonnet 4.6 (claude-4.6-sonnet-medium) VERIFIED — 3 prompts including a full live info/array/shares/vms/docker/online overview rendered to a Markdown table; error-path prompt handled correctly without invented recovery out/verification/cursor-agent-sonnet-mcp-call.txt
opencode v1.14.30 DeepSeek v4 Flash via opencode-go/deepseek-v4-flash VERIFIED on schema-only path; live execute hit a mid-test upstream CSRF flip — schema smoke (102 ops) green; the live overview produced valid Promise.all typed-query code on the first try and the model handled the upstream Invalid CSRF token / 401 gracefully (explained, suggested re-auth, did not flail) out/verification/opencode-deepseek-mcp-call.txt

See examples/unraid-expert-agent/ for the persona (AGENTS.md), a vetted set of sample prompts, and cross-platform install snippets.

NOT verified by us (and where help is welcome): every agent / IDE client beyond cursor-agent CLI and opencode — Cursor IDE chat panel, Claude Code, Claude Desktop, VS Code + Copilot, Codex CLI, Continue, Cline, Aider, Zed, MCP Inspector (CLI + UI); the Streamable HTTP transport in multi-tenant mode; the Cloudflare Workers entry (scaffolded but not deployed); any non-VM mutation (Docker container start/stop, share/disk operations, parity ops); any Unraid box other than the maintainer's. We need testers — please file verification reports and bug reports with whatever you find. See CONTRIBUTING.md for the rules.

Why "code mode"?

The default MCP pattern is one tool per API operation. A typical Unraid 7.2 schema has ~57 queries and ~45 mutations across 240+ types — registering all of those as discrete MCP tools blows past every commercial agent's tool-list cap.

Code mode flips that: instead of a hundred tools, you get two. The LLM uses search to figure out what to call (no network), then writes a tiny JS snippet for execute that talks to the live API. See Cloudflare's code mode post for the full pattern, or the architecture doc for the per-module breakdown.

Highlights

  • Two tools. search is read-only; execute runs sandboxed JS that calls real Unraid GraphQL.
  • Typed convenience calls. unraid.local.query.<fieldName>({ args, fields }) and unraid.local.mutation.<fieldName>({ args, fields }) synthesise the GraphQL document for you using introspected arg types.
  • Raw escape hatch. unraid.local.graphql({ query, variables }) posts any document; unraid.local.request({ method, path, body }) covers the rare non-GraphQL endpoints.
  • Single- and multi-user modes. Single-user via env vars (stdio), multi-user via X-Unraid-Api-Key HTTP headers.
  • Per-tenant TLS. Custom CA bundles or insecure mode survive into a per-request undici dispatcher.
  • Bundled SDL fallback. Server can boot without an Unraid server — it parses the latest committed src/spec/local-fallback.graphql so search is immediately useful.
  • Cloudflare Workers entry. A scaffold using @cloudflare/codemode ships in cf-worker/. See cf-worker/README.md.

Quick start

git clone https://github.com/jmpijll/unraid-code-mode-mcp
cd unraid-code-mode-mcp
npm install --legacy-peer-deps
cp .env.example .env
# edit .env — set UNRAID_BASE_URL + UNRAID_API_KEY
npm run dev

Wire it into your MCP client. For Cursor, add to .cursor/mcp.json:

{
  "mcpServers": {
    "unraid": {
      "command": "node",
      "args": ["/absolute/path/to/unraid-code-mode-mcp/dist/index.js"]
    }
  }
}

(Run npm run build first if you point at dist/. Or use npx tsx against src/index.ts for live development.)

Creating an Unraid API key

The API key is what authenticates the MCP server's calls to your Unraid box. Two ways to mint one:

Option A — Web UI. Go to Settings → Management Access → API Keys and create a key with the ADMIN role (or scope it down to whatever you actually want the agent to do). Copy the value into UNRAID_API_KEY.

Option B — CLI on the Unraid box. SSH in and run:

unraid-api apikey --create --name "mcp" --roles ADMIN --json

The JSON output contains a key field; that's UNRAID_API_KEY.

UNRAID_BASE_URL should be the URL you'd visit in a browser to reach the web UI (no path, no trailing slash) — e.g. https://tower.local or https://192.168.1.10.

TLS on Unraid

Unraid 7.2+ usually serves over HTTPS using a self-signed *.unraid.net certificate fronted by the LAN proxy. The MCP server has three options:

  1. Recommended: install the Unraid root CA on the host, or fetch it and pass UNRAID_CA_CERT_PATH=/path/to/ca.pem.
  2. Lab use: UNRAID_INSECURE=true skips verification. Logged on every request.
  3. Multi-tenant: clients can supply X-Unraid-Ca-Cert and/or X-Unraid-Insecure: true per request.

See docs/security.md for the full picture.

Sample interactions

Discover the schema:

// search tool
searchOperations('docker', 10).map(function (op) { return op.name + ' (' + op.kind + ')'; });
// search tool — drill into a single op
getOperation('info');

Run a query (verified live on Unraid 7.2):

// execute tool
const info = await unraid.local.query.info({
  fields: ['os { distro release kernel uptime }', 'cpu { manufacturer brand cores threads }'].join(' '),
});
return info;

Read multiple things — sequential await and Promise.all both work:

// execute tool — sequential awaits (fine; full canonical async)
const info = await unraid.local.query.info({ fields: 'os { distro }' });
const arr = await unraid.local.query.array({ fields: 'state' });
const shares = await unraid.local.query.shares({ fields: 'name free used size' });
return { info, arr, shares };
// execute tool — parallel batch (faster when calls are independent)
const [info, arr, shares, online] = await Promise.all([
  unraid.local.graphql({ query: 'query { info { os { distro release kernel } cpu { brand cores threads } } }' }),
  unraid.local.graphql({ query: 'query { array { state } }' }),
  unraid.local.graphql({ query: 'query { shares { name free used size } }' }),
  unraid.local.graphql({ query: 'query { online }' }),
]);
return { info, arr, shares, online };

Run a mutation:

// execute tool
return await unraid.local.mutation.archiveAll({});

Fall back to raw GraphQL:

// execute tool
const data = await unraid.local.graphql({
  query: 'query { array { state capacity { kilobytes { free total } } } }',
});
return data;

Documentation

Verifying your install

npm run build
npm test                  # 65 unit + integration tests
npm run test:sandbox      # QuickJS host-bridge stress (no Unraid box needed)
npm run smoke:inspector   # MCP Inspector CLI smoke against built dist (no Unraid box needed)

If smoke:inspector prints OK: both 'search' and 'execute' tools exposed by dist/index.js, your build is wire-compatible with any MCP client. The sandbox stress doubles as the regression bar for the Promise-callback host bridge.

Multi-user / multi-tenant

Run with MCP_TRANSPORT=http and without env credentials. The MCP HTTP transport listens on POST /mcp + GET /health, and every request must carry:

  • X-Unraid-Api-Key
  • X-Unraid-Base-Url
  • X-Unraid-Insecure (optional, true to skip TLS verification)
  • X-Unraid-Ca-Cert (optional, PEM-encoded CA bundle)

Origin allowlist defaults to localhost; tune via MCP_HTTP_ALLOWED_ORIGINS. See docs/multi-tenant.md.

Verification status

What we have directly verified so far:

Layer How Result
Unit tests Vitest, 56 specs across spec loader, dispatcher, sandbox, HTTP client, multi-tenant context, and server transports ✅ all green
Integration tests In-process node:http GraphQL mock + InMemoryTransport against createMcpServer; covers sequential awaits, Promise.all, mixed query/mutation/raw GraphQL, error propagation, and the per-execute call budget ✅ green
QuickJS host-bridge stress npm run test:sandbox — 25 sequential awaits, 10-way Promise.all, mixed patterns, rejection propagation through await ✅ green; this is the regression bar after the asyncify → sync + Promise-callback rewrite
SDL fallback (introspection disabled) Server boots without an Unraid box, parses bundled src/spec/local-fallback.graphql (pinned to unraid/api@v4.33.0), search is immediately usable; runtime fallback path also exercised by unit tests against a mock that returns INTROSPECTION_DISABLED ✅ green; the INTROSPECTION_DISABLED error returns a human-readable diagnostic with a remediation hint (unraid-api developer --sandbox true) instead of HTTP 400
Live read sweep on a real Unraid 7.2 box scripts/mcp-call.mjs driving stdio transport against the maintainer's homelab: info, array, shares, vms, docker, online ✅ all queries returned real data; sequential awaits and Promise.all both worked end-to-end with real GraphQL latency
Live mutation round-trip on the same box VM SHUTOFF → RUNNING → SHUTOFF cycle via vmStart / vmStop mutations, with state polled via vms.domain.state between transitions ✅ full cycle completed; error propagation verified by attempting vmStart on an already-running VM (returns Failed to set VM state: Invalid state transition from RUNNING to RUNNING cleanly through await)
Linter + formatter + typecheck npm run lint / npm run format:check / npm run typecheck ✅ clean

What is not yet verified (and where help is welcome):

  • Any agent / IDE client. All live verification so far has been through scripts/mcp-call.mjs driving the stdio transport directly. Cursor (chat panel), Claude Code, Claude Desktop, VS Code + Copilot, Codex CLI, Continue, Cline, opencode, Aider, Zed, the MCP Inspector (CLI and UI) — all wired but NOT verified by us. End-to-end LLM-mediated invocation is the most useful thing testers can report on.
  • Streamable HTTP transport in multi-tenant mode. The transport is wired and unit-tested; a real multi-tenant deployment behind a reverse proxy with header-based credentials is not.
  • Cloudflare Workers entry. cf-worker/ is scaffolded against @cloudflare/codemode but the Web Request/Response ↔ MCP SDK Node-stream adapter is not implemented, and the Worker is not deployed anywhere. Tracked in cf-worker/README.md.
  • Mutations beyond VM start/stop. Docker container start / stop, parity check start / cancel, share / disk operations, user / API-key management, and the full mutation surface are wired through the typed dispatcher but not live-verified. Probing them blindly against live hardware is unsafe; we want testers with redundant homelabs.
  • Unraid Connect cloud surface. unraid.connect.* is reserved in TenantContext and not yet implemented. The current server only talks to a controller you can reach over the LAN.
  • Real Unraid boxes other than the maintainer's homelab. A single Unraid 7.2 box is not enough to generalise resilience claims — different array configs, plugin sets, network topologies, and Unraid versions will all surface different edge cases.
  • Long-running soak / stability under sustained load.

Roadmap

Done in v0.1.0-beta.2 and v0.1.0-beta.3:

  • Expose the sandbox wall-clock deadline as UNRAID_EXECUTE_TIMEOUT_MS (1 s – 10 min, default 30 s). Useful for slow-booting VMs and for very large Promise.all batches against a controller under load. (beta.2)
  • CSRF-aware error decoration — when an Unraid box returns extensions.code: UNAUTHENTICATED + Invalid CSRF token, the MCP server adds a remediation hint pointing at API key re-mint, the curl sanity check, and the box-side log path. See docs/security.md. (beta.2)
  • MCP serverInfo.version reads from package.json at runtime — no more hand-stamped version drift between releases. (beta.2)
  • End-to-end LLM-mediated invocation verification through cursor-agent (Claude Sonnet 4.6) and opencode (DeepSeek v4 Flash). See the verification matrix above. (beta.2)
  • Auto-bump bundled SDL pin.github/workflows/update-spec.yml runs weekly, detects new unraid/api releases, regenerates src/spec/local-fallback.graphql, and opens a PR with a release-notes link for human review. (beta.3)
  • npm run smoke:inspector — local mirror of the CI MCP Inspector smoke. Boots the built dist/index.js server, requests tools/list, asserts both tools are exposed. CI now uses the same script. (beta.3)

Still open (rough order, highest-leverage first):

  • More LLM clients verified — Claude Code CLI, Claude Desktop, MCP Inspector UI (CLI smoke is in CI), VS Code + Copilot. Roadmap item, gating for 1.0.0.
  • Streamable HTTP multi-tenant deployment verified against a real reverse proxy with rotating per-tenant credentials.
  • Mutation verification matrix beyond VM start/stop — Docker container lifecycle, parity checks, share/disk ops — once we have testers with redundant hardware.
  • unraid.connect.* namespace for the Unraid Connect cloud API. Reserved in TenantContext today, not yet implemented.
  • Cloudflare Workers transport adapter (the bridge from Web Request/Response to the MCP SDK's Node IncomingMessage/ServerResponse). Tracked in cf-worker/README.md.
  • NPM publish — reserved for 1.0.0. The package is "private": true until then.

Contributing

See CONTRIBUTING.md. Use Conventional Commits (feat:, fix:, chore:, docs:, test:, ci:).

License

MIT — see LICENSE.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选