muninn-mcp
by Vanilla-Game
README.md
# muninn-mcp
Read-only [Model Context Protocol](https://modelcontextprotocol.io/) server for
one [Muninn](https://github.com/Vanilla-Game/muninn-plugin) Minecraft backend.
One process always represents exactly one game backend. It has one Muninn base
URL, one backend bearer token, and one discovered `server_id`. Run a separate
process for each backend because installed plugins and capabilities can differ.
The default MCP transport is the official stateful Streamable HTTP transport
from the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk).
Stdio remains available as a fallback.
## Requirements
- Node.js 20 or newer
- a reachable Muninn plugin HTTP API
- the backend bearer token from `plugins/Muninn/config.yml`
This implementation targets Muninn plugin API `0.1.0` at plugin commit
`6ef77aafb1cb0f634020d48ca5621885bb103940`.
## HTTP-first start
```bash
npm ci
npm run build
MUNINN_BASE_URL=http://127.0.0.1:8781 \
MUNINN_AUTH_TOKEN='<Muninn backend token>' \
MUNINN_EXPECTED_SERVER_ID=survival \
MUNINN_MCP_AUTH_TOKEN='<separate MCP client token>' \
npm start
```
Defaults:
- transport: `http`
- bind: `127.0.0.1`
- port: `3000`
- MCP endpoint: `http://127.0.0.1:3000/mcp`
- readiness endpoint: `http://127.0.0.1:3000/healthz`
The incoming `MUNINN_MCP_AUTH_TOKEN` is deliberately separate from
`MUNINN_AUTH_TOKEN`. The first protects MCP clients → this process; the second
protects this process → the Minecraft backend. Never reuse them.
An HTTP MCP client connects to the URL and supplies the static token:
```json
{
"mcpServers": {
"muninn-survival": {
"url": "http://127.0.0.1:3000/mcp",
"headers": {
"Authorization": "Bearer <MUNINN_MCP_AUTH_TOKEN>"
}
}
}
}
```
The exact client configuration envelope is client-specific; the URL, standard
`Authorization` header, and Streamable HTTP protocol are not.
Readiness needs no token and exposes no configuration secrets:
```bash
curl http://127.0.0.1:3000/healthz
```
The process first discovers the backend through `/health` and `/capabilities`.
It only starts listening after backend identity and capabilities have passed
fail-fast validation.
## Stdio fallback
```bash
MUNINN_TRANSPORT=stdio \
MUNINN_BASE_URL=http://127.0.0.1:8781 \
MUNINN_AUTH_TOKEN='<Muninn backend token>' \
MUNINN_EXPECTED_SERVER_ID=survival \
npm start
```
Example stdio client entry:
```json
{
"mcpServers": {
"muninn-survival": {
"command": "node",
"args": ["/absolute/path/to/muninn-mcp/dist/index.js"],
"env": {
"MUNINN_TRANSPORT": "stdio",
"MUNINN_BASE_URL": "http://127.0.0.1:8781",
"MUNINN_AUTH_TOKEN": "<Muninn backend token>",
"MUNINN_EXPECTED_SERVER_ID": "survival"
}
}
}
}
```
## Configuration
### MCP transport
| Variable | Default | Meaning |
|---|---:|---|
| `MUNINN_TRANSPORT` | `http` | `http` or `stdio`. |
| `MUNINN_MCP_BIND` | `127.0.0.1` | HTTP listener hostname or IP. |
| `MUNINN_MCP_PORT` | `3000` | HTTP listener port, 1–65535. |
| `MUNINN_MCP_PATH` | `/mcp` | Exact Streamable HTTP endpoint path. |
| `MUNINN_MCP_AUTH_TOKEN` | unset | Incoming static bearer. Optional only on loopback; required for every non-loopback HTTP bind. |
| `MUNINN_MCP_ALLOWED_HOSTS` | loopback hosts | Comma-separated hostnames without ports. Required for wildcard binds such as `0.0.0.0`. |
| `MUNINN_MCP_ALLOWED_ORIGINS` | none | Exact comma-separated browser origins allowed for CORS. Browser Origin requests are rejected by default. |
For a private-network listener:
```bash
MUNINN_MCP_BIND=0.0.0.0 \
MUNINN_MCP_ALLOWED_HOSTS=minecraft-admin.internal,192.0.2.20 \
MUNINN_MCP_AUTH_TOKEN='<high-entropy token>' \
npm start
```
The built-in listener is plain HTTP. Do not expose it directly to the public
internet. Keep it on loopback/private networking or place TLS and appropriate
network controls in front of it.
### Muninn backend
| Variable | Required | Default | Meaning |
|---|---:|---:|---|
| `MUNINN_BASE_URL` | yes | — | Backend origin or API root. A bare origin gets `/api/v1/` appended. |
| `MUNINN_AUTH_TOKEN` | yes | — | Bearer token accepted by the Muninn plugin. |
| `MUNINN_EXPECTED_SERVER_ID` | no | — | Fail-fast backend identity pin; strongly recommended. |
| `MUNINN_TIMEOUT_MS` | no | `15000` | Per-request timeout, 100–120000 ms. |
| `MUNINN_DEFAULT_PAGE_SIZE` | no | `100` | Explicit default for paginated tools. |
| `MUNINN_MAX_PAGE_SIZE` | no | `100` | MCP-side page cap, maximum 1000. |
| `MUNINN_MAX_LOOKUP_SECONDS` | no | `2592000` | MCP-side CoreProtect time-window cap. |
| `MUNINN_MAX_RADIUS` | no | `128` | MCP-side CoreProtect radius cap. |
Backend limits remain authoritative and may be stricter.
## HTTP security and lifecycle
- Host validation is port-independent and deny-by-default.
- Browser requests with an `Origin` header are denied unless the exact origin is
allowlisted. CORS never uses `*` and never enables credentials.
- Incoming auth uses constant-time comparison of SHA-256 token digests.
- Request bodies are parsed only after MCP authentication and are capped at
256 KiB.
- Neither backend nor incoming bearer values are logged or returned in errors,
including nested backend payloads.
- Each initialize request gets a cryptographically random stateful MCP session,
its own official `StreamableHTTPServerTransport`, and its own `McpServer`.
- Subsequent POST/GET/DELETE requests require a valid `Mcp-Session-Id`.
- HTTP DELETE terminates a session. `SIGINT`/`SIGTERM` stop accepting requests,
close all active transports/SSE streams, and close the HTTP server.
- Sessions are in memory and are not resumable across process restarts; clients
initialize again after a restart.
## Capability-aware tools
Only tools whose endpoint is present in an enabled module's capability report
are registered. CoreProtect tools also require the corresponding feature flag.
Restart the process after backend plugin/capability changes to refresh
`tools/list`.
All tools are annotated read-only, non-destructive, and idempotent. The Paper
batch endpoint uses HTTP POST but does not mutate game state.
### Core and composite
- `server_status`
- `investigate_block` — bounded Paper/CoreProtect/WorldGuard context with
independent probe results
### CoreProtect primitives
- `coreprotect_block_lookup`
- `coreprotect_container_lookup`
- `coreprotect_item_lookup`
- `coreprotect_inventory_lookup`
- `coreprotect_chat_lookup`
- `coreprotect_command_lookup`
- `coreprotect_session_lookup`
- `coreprotect_sign_lookup`
- `coreprotect_username_lookup`
- `coreprotect_queue_lookup`
### Paper primitives
- `paper_get_container`
- `paper_batch_containers` (1–64 locations)
- `paper_get_player_inventory`
- `paper_get_player_ender_chest`
- `paper_get_player_state`
- `paper_get_player_stats`
- `paper_list_players`
- `paper_get_server_info`
- `paper_list_entities`
- `paper_get_block`
### CMI primitives
- `cmi_list_players`
- `cmi_get_player`
- `cmi_get_player_homes`
- `cmi_list_warps`
- `cmi_list_jails`
### WorldGuard primitives
- `worldguard_list_regions`
- `worldguard_get_region`
- `worldguard_regions_at`
- `worldguard_flags_at`
Paginated tools always send explicit bounded `offset`/`limit` values and return
the backend's `has_more` marker for deliberate follow-up.
## Errors
Muninn envelopes become readable MCP tool errors with stable codes, safe
details, HTTP status, backend ID, guidance, and retryability where applicable:
`UNAUTHORIZED`, `NOT_FOUND`, `BAD_REQUEST`, `SYNC_TIMEOUT`,
`MODULE_DISABLED`, `FEATURE_UNAVAILABLE`, `LIMIT_EXCEEDED`,
`PLAYER_OFFLINE`, and `INTERNAL`.
Transport-side errors include `TIMEOUT`, `NETWORK_ERROR`, `CANCELLED`,
`INVALID_RESPONSE`, and `SERVER_ID_MISMATCH`.
HTTP routing/auth errors use bounded JSON/JSON-RPC bodies and never echo Host,
Origin, authorization values, request bodies, or internal exceptions.
## Verification
```bash
npm ci
npm run check
npm test
```
The suite covers:
- HTTP-default and stdio process startup/shutdown;
- official `StreamableHTTPClientTransport` initialize, initialized notification,
`tools/list`, `tools/call`, SSE, and DELETE lifecycle;
- incoming auth 401, Host/Origin/CORS policy, invalid methods and sessions;
- non-zero invalid configuration;
- recursive secret redaction;
- all 29 atomic tool-to-endpoint mappings and capability filtering.
### Real plugin Docker harness
The cross-project test starts the adjacent real
Paper+CoreProtect+CMI+WorldGuard fixture, starts the built HTTP MCP process,
connects through the official Streamable HTTP client, calls
`paper_get_server_info`, and removes all temporary processes/containers:
```bash
npm run test:e2e:harness
```
By default the plugin checkout is expected at `../muninn-plugin`:
```bash
MUNINN_PLUGIN_DIR=/absolute/path/to/muninn-plugin npm run test:e2e:harness
```
An already-running backend can be tested directly:
```bash
MUNINN_E2E_BASE_URL=http://127.0.0.1:8781 \
MUNINN_E2E_AUTH_TOKEN='<backend token>' \
MUNINN_E2E_SERVER_ID=survival \
npm run test:e2e
```
The stdio fallback remains covered by the normal test suite.
## Releases and npm publication
Release Please watches conventional commits on `main`, maintains a release PR,
updates `CHANGELOG.md`, `package.json`, `package-lock.json`, and the release
manifest, then creates a `vX.Y.Z` GitHub Release when that release PR is merged.
The same workflow checks and tests the released commit before publishing
`@vanilla-game/muninn-mcp` as a public npm package with provenance.
The package is not published yet, so the first release needs a short-lived npm
granular access token with permission to create packages in the `vanilla-game`
scope. Store it as the `NPM_TOKEN` GitHub Actions secret. After the bootstrap
publish succeeds, configure npm Trusted Publishing for:
- GitHub organization: `Vanilla-Game`
- repository: `muninn-mcp`
- workflow filename: `release-please.yml`
- allowed action: `npm publish`
Then remove the `NPM_TOKEN` repository secret. Future releases use GitHub OIDC
through the workflow's `id-token: write` permission instead of a long-lived npm
credential. The npm package's repository URL must continue to match this GitHub
repository exactly.
By default Release Please uses the workflow's `GITHUB_TOKEN`. An optional
`RELEASE_PLEASE_TOKEN` GitHub secret can supply a GitHub App or fine-grained PAT
when repository policy requires release PR events to trigger other workflows.
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing