


A standalone BepInEx plugin that exposes a localhost HTTP endpoint for driving
Valheim's in-game console remotely and returning the console output directly in the
response. Built to give an agent (Claude Code) reproducible, scriptable access to game
state — e.g. running console commands like pos and reading what they print, without
typing into the in-game console or round-tripping through dump files.
This mod opens an unauthenticated HTTP endpoint that runs arbitrary Valheim
console commands (including anything devcommands unlocks). There is no token, no
password, no per-request check beyond the optional allow/deny list below.
127.0.0.1) only by default, so it is not reachable
from other machines. Treat any change to server.host as exposing full console
control to whoever can reach that address — don't bind it to 0.0.0.0 or a LAN IP.commands.allow / commands.deny
list (see Config) so only the commands you intend can run.Built and tested against Valheim (BepInEx pack
denikson-BepInExPack_Valheim-5.4.2333, BepInEx 5.4.x). It only depends on
Valheim's own Console/Terminal, so it should be resilient across game patches,
but it is not tied to any specific game build.
BepInEx/plugins/ValheimMCP/ (loaded once, not hot-reloaded), so the
listener stays up across F6 reloads of the mods you're iterating on and never fights
for its port.HttpListener thread accepts requests and marshals each onto Unity's
main thread (MainThreadDispatcher) before touching any Valheim API.Terminal.AddString while a
command runs — no private-field reflection.Console/Terminal, which
already has every registered command.| Method | Path | Body / Query | Returns |
|---|---|---|---|
| POST | /mcp |
JSON-RPC 2.0 (MCP) | MCP response (application/json) |
| GET | /health |
— | {ok, inGame} |
| GET | /commands |
— | {ok, commands:[{name,description}]} |
| POST | /command |
raw command line, or ?text= |
{ok, ran, output:[...], error?} |
| GET | /log |
?since= ?maxLines= ?contains= ?regex= |
{ok, cursor, matching, dropped, lines:[...]} |
| GET | /sse |
— | 501 Not Implemented (no SSE transport) |
The plain /health, /commands, /command routes are for curl/scripting.
/mcp speaks the protocol Claude Code consumes.
The plugin implements the MCP Streamable-HTTP transport (JSON-RPC 2.0) directly,
with no external dependencies — a hand-rolled JSON parser/writer and stateless
application/json responses (no SSE). Tools:
run_command(text) — run a console command, return captured output.list_commands() — all registered console commands.health() — is a world loaded.render_view(x, z, [y, yaw, pitch, dist, size]) — render the location with an
independent off-screen camera (never touches the player's view) and return
the PNG inline. The off-screen camera is named valheimmcp_render_cam, so a mod
that can draw debug overlays into a named camera can target it to render those
overlays into only this view, leaving the player's screen untouched.wait_for_log(pattern, [regex, timeoutMs]) — block until a log line matches,
then return it (streams observed lines as progress when the client sends a
progressToken). For waiting on an async event (e.g. a mod hot-reload finishing).get_log([since, maxLines, contains, regex]) — tail recent log lines from an
in-memory ring buffer (the MCP server, the game, and every other mod), returning
immediately. Omit since for the latest lines; pass back the cursor from the
result header to poll only what's new. Filter with contains/regex. Lets you
read logs even on a dedicated server whose LogOutput.log isn't reachable.Register it with Claude Code (game can be launched after; the connector reconnects):
claude mcp add --transport http valheim http://127.0.0.1:8731/mcp
Use 127.0.0.1, not localhost — the server binds IPv4 loopback only, and
localhost may resolve to IPv6 ::1 first (which isn't bound). This IPv4-only
bind is intentional: it keeps the endpoint strictly local.
curl -s 127.0.0.1:8731/health
curl -s 127.0.0.1:8731/commands
curl -s -X POST 127.0.0.1:8731/command --data 'pos'
BepInEx/config/valheimmcp.yml (written with defaults on first run; parsed by the
dependency-free MiniYaml reader):
server:
host: 127.0.0.1 # loopback only — endpoint is unauthenticated
port: 8731
commandTimeoutMs: 15000
render:
defaultSize: 768 # render_view size when 'size' is omitted
minSize: 256
maxSize: 1280
wait:
defaultTimeoutMs: 120000 # wait_for_log timeout when 'timeoutMs' is omitted
maxTimeoutMs: 600000
heartbeatMs: 5000 # SSE progress heartbeat while a wait is pending
log:
bufferCapacity: 2000 # in-memory ring of recent log lines retained for get_log
defaultLines: 200 # get_log lines returned when 'maxLines' is omitted
maxLines: 1000
# Access control for run_command (and POST /command). 'deny' always wins; if
# 'allow' is non-empty, ONLY matching commands run. Match by command name;
# trailing '*' is a prefix wildcard (e.g. "spawn*").
commands:
allow: []
deny: []
From Thunderstore (recommended): install with r2modman or the Thunderstore Mod Manager. It pulls in the BepInEx pack automatically.
Manual: install BepInExPack_Valheim,
then drop ValheimMCP.dll into BepInEx/plugins/ValheimMCP/. Launch the game once
to generate BepInEx/config/valheimmcp.yml, then edit it if needed.
dotnet build src/ValheimMCP/ValheimMCP.csproj -c Release
Output lands directly in BepInEx/plugins/ValheimMCP/. The build paths in the
.csproj assume a local Steam install and an r2modman profile named
valheim-modding; override ValheimDir / R2ModmanProfile if yours differ.
CI can't compile this — it needs Valheim's (non-redistributable) managed
assemblies — so releases are built locally. The version lives in one place,
<Version> in ValheimMCP.csproj: it flows into the assembly version, the
generated PluginInfo constants (used by Plugin.cs / McpServer.cs), and the
packaged manifest.json (synced by scripts/package.sh). Bump it there, add a
CHANGELOG.md entry, then:
./scripts/package.sh # -> dist/ValheimMCP-<version>.zip
gh release create v<version> dist/ValheimMCP-<version>.zip --generate-notes
Upload the same zip to Thunderstore. The
GitHub Actions workflow (.github/workflows/release.yml) can publish the release
for you on a v* tag if you git add -f the built zip onto the tagged commit.
/mcp), capturing synchronous
console output. Usable via curl or as Claude Code MCP tools.GET /file route (and/or inlining written: <path> contents) so
commands that dump to disk return their payload too; typed introspection tools for
live game state.MIT © 2026 myrcutio