gmod-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@gmod-mcp debug the Lua error on server start"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
gmod-mcp
A local-first MCP server for AI-assisted Garry's Mod addon development. It plugs into
Claude Code the way claude-in-chrome does: agents discover the MCP tools and iterate on
their own — lint → boot → observe runtime → patch → validate. No web app, no UI.
Two halves:
gmod-mcp— the MCP server, TypeScript/Node, stdio transport.gmod_mcp_bridge(addon/gmod_mcp_bridge) — a GLua addon exposing server state over a file transport inside GMod's DATA sandbox. The daemon and srcds share a filesystem, so no network is involved. This is not a stylistic choice: GMod'sHTTP()was measured not to reach a localhost daemon from a dedicated server.
What it buys you
You talk about the addon in plain language while the model sees the actual game state — "why does this net message never fire?", "fix this error", "show me that menu". The agent lints, boots the server, reads structured Lua errors, patches, reloads and re-validates, in a loop.
Related MCP server: niua-godot-mcp
Install
pnpm install && pnpm build
# Register the server with Claude Code (project scope, committable):
node dist/index.js install
# -> writes <repoRoot>/.mcp.jsonCLI alternative:
claude mcp add gmod-mcp -e GMOD_MCP_REPO=<repoRoot> -- node <abs>/dist/index.js
The daemon finds the repo root by walking up from the cwd looking for tools/lint.sh and
CLAUDE.md, or via GMOD_MCP_REPO, or via .gmod-mcp/config.json.
The bridge addon must be mounted by your dedicated server. Symlink it rather than copying, so
a SteamCMD validate can never overwrite it:
ln -s /path/to/gmod-mcp/addon/gmod_mcp_bridge \
/path/to/srcds/garrysmod/addons/gmod_mcp_bridgeTransport
Server realm. The daemon writes srcds/garrysmod/data/gmod_mcp/cmd/<id>.json atomically;
the addon reads it, runs it, writes res/<id>.json and deletes the command. Events (Lua
errors, bridge_up) arrive as evt/<n>.json. The daemon polls res/ and evt/. No port, no
token, no handshake.
One daemon per transport directory, enforced by daemon.lock (PID inside, stale locks
reclaimed). The protocol consumes res/, so a second daemon reading the same directory
deletes results the first one is waiting for: the command really ran, the result really was
written, and the caller still times out. That is what happens the moment a second Claude Code
session is opened on the same repo — measured 2026-07-25, and it cost forty minutes because
the symptom accuses the game. Every bridge tool timed out while srcds was healthy and the
addon mounted; reconnecting the client and restarting the server changed nothing, since the
interfering state lived in a third process. Diagnose it with:
ps -eo pid,etime,args | grep gmod-mcp/dist/index.js # more than one line is the bugA daemon that cannot take the lock keeps its MCP tools but touches nothing: no scanner, no
commands written, and every bridge call refuses with the owner's PID. health reports the
same under bridge.transport. A res/ file matching no in-flight command is now left alone
for a grace period rather than deleted on sight — blind cleanup is what turned coexistence
into an outage — and the count of such files is reported, because on a single-daemon setup it
should be zero.
Client realm. The daemon writes a cl command down the same file channel; the server addon
routes it to the client over a net message; the client runs it and sends the result back, in
chunks reassembled server-side into res/. No HTTP, and the client can be on any machine as
long as it is connected to the server. The target is the first player, or args.player
(SteamID).
Chunks are small and paced one per frame, drained one result at a time, and capped at 48 chunks per result. All three came from the same failure, met twice.
An early version pushed 60 KB chunks in a single frame, which overflowed the client-to-server
reliable channel (send reliable stream overflow) and timed the client out. That failure is
persistent and silent — once the channel is swamped, no net message from that client gets
through, so every client tool times out and the relay looks broken when it has merely been
flooded.
A relayed command the client never answers fails server-side after 20 s, under the
daemon's 30 s round trip, and says so: which tool, how many chunks arrived before the transfer
was discarded, and that srcds itself is fine. Half-received chunks are dropped with it — a
partial reassembly is not a result. read_runtime carries the relay's state (waiting,
oldest_seconds, oldest_tool, partial_transfers), because a stuck channel and an idle one
used to read identically from the daemon.
Per-frame pacing alone was not enough. Measured on a real player: a full-screen capture at quality 80 came to 424 KB and 62 chunks and dropped them from the server, while a full-screen q70 capture (100 KB, 15 chunks) had gone through fine minutes earlier. Two things were wrong. Each result had its own timer, so several in flight summed on the same channel and the pacing stopped meaning anything — a caller retrying a command it believes was lost has two captures in flight, and the second is what tips it over. And nothing bounded a single result. Now one drain serialises every result, and anything over 48 chunks is refused with its size and a suggestion instead of sent. The default half-scale capture is about six chunks.
Iteration loop
edit → lint → (boot) → observe → patch → reload → validate → repeat
The daemon shells out to the host project's tools/lint.sh, start-server.sh and
server-log.sh, parsing file:line: and exit codes. It also encodes the traps that cost real
debugging time: the boot boundary inside an accumulating console.log, waiting for
InitPostEntity before reading cvars, the queueing latency of game.ConsoleCommand, and
NUL-safe log reads.
Tool catalogue
Local (daemon) — health, lint, start_server, stop_server, sync_config,
read_logs, package, patch_file, restore_patch, reload_file, reload_addon,
validate, run_iteration.
Server, reading — batch, read_runtime, read_players, read_entities,
inspect_entity, read_hooks, read_convars, read_net_messages, read_timers,
run_console_command, send_debug, run_test, run_lua (guarded, optional extension).
Server, acting (all guarded) — spawn_entity, world_edit, set_player_state,
force_hook.
Client (via bridge) — read_view, client_input, read_panels, inspect_panel,
capture_screen, read_console, read_client_convars.
health also asks the addon which handlers it registered and reports any the daemon
declares but the game does not have. That gap used to surface only as an "unknown handler"
after a full round trip, or — when a whole include was missing — not at all. It reports the
transport state alongside: whether this daemon owns the directory and who holds it otherwise,
what is in flight, how long since the addon last answered, and how many res/ files matched
nothing of ours. Start every "nothing responds" investigation there.
capture_screen returns a real image content block. Returned as text, a base64 JPEG is
billed to the model token by token and still cannot be looked at, so the "see" half of an
act/see loop silently does nothing while every test passes.
Acting
The reading tools diagnose; the acting ones set up what is worth diagnosing. Server-side,
spawn_entity places something, world_edit moves, freezes, heals, arms or removes it,
set_player_state sets money, job, salary or RP name, and force_hook runs a gamemode
hook directly.
Money goes through the r-capitalism ledger when it is loaded. That ledger holds an audited
invariant — sum(balances) == issued - burned — and a raw addMoney would move a balance
without an entry, leaving the drift permanently off zero. A debugging tool must not corrupt
what is being debugged. Amounts are integer cents, as the rest of that server economy is.
force_hook coerces tagged arguments, because JSON cannot carry a game object:
{"__ent": 3}, {"__ply": "STEAM_0:1:2"}, {"__vec": [x,y,z]}, {"__ang": [p,y,r]}.
Client-side, client_input drives the connected GMod client — movement, aim, key holds,
Derma clicks, typing, chat — and read_view reports eye position, aim trace, cursor,
hovered panel and keyboard focus. read_view is the cheap half of an act-then-look loop:
one chunk, no image, and it answers "am I aimed at the door" without a screenshot.
client_input is bounded in Lua rather than guarded behind a confirmation. It drives a
real person's machine, and a prompt clicked two hundred times is not a safety property:
holds expire, durations are seconds and clamped to five, a 30s deadline resets everything,
and gmod_mcp_release in the client console returns control without involving the daemon.
Batching
A bridge round trip costs about 0.4s: the addon polls at 0.25s and the daemon scans at
0.15s. Any sequence that acts and then looks pays that per gesture. batch carries up to
32 server steps in one command instead:
{ "steps": [{"tool": "read_runtime"},
{"tool": "read_timers", "args": {"names": ["gmod_mcp_bridge_poll"]}}],
"settleMs": 100 }Measured against a live DarkRP server: three steps in 0.19s, versus roughly 0.75s as three separate calls.
A failing step is data, not a transport error — each step reports its own ok/data/
error, stopOnError marks the rest skipped and records abortedAt, so the caller sees
the whole shape of the batch. settleMs pauses between steps, which is what makes
act-then-look honest: without it a step observes the frame before the previous one landed.
Guarded tools are checked on both sides. batch is a single unguarded definition, so
without the check a run_lua step would bypass the confirmation its own gate demands; the
daemon resolves each step against the registry and the Lua runner repeats the check.
Any argument may instead be {"__step": 1, "get": "index"}, read from an earlier step's
result. Without it, spawning something and then acting on it costs two round trips — the
caller cannot know the EntIndex until the spawn has answered, which is the round trip
batching exists to remove.
Steps share a server tick unless settleMs is set, and some engine effects only land at
end of frame: Entity:Remove() is deferred, so a step reading the entity back still finds
it valid and the agent concludes the removal failed. Set settleMs (100 is usually enough)
whenever a step must observe an earlier one.
Client-realm steps are refused explicitly rather than silently dropped — a batch runs
inside the server addon, and cl tools are relayed over net. So an act-then-look loop on
the client is currently two round trips, not one.
All three realms have been exercised against a live DarkRP server: the server tools on
rp_nycity_day at tick 33, and the client tools against a connected GMod client —
read_panels returning a live VGUI tree and capture_screen returning a complete 1920x1080
JPEG. batch was proven the same way, including its failure paths: a step raising returns
the real Lua error with file:line, the rest come back skipped, and a run_lua step is
refused unconfirmed and executes confirmed.
Security
Guarded tools (
run_lua) requireconfirm: trueor membership intoolAllowlist; otherwise they are refused without executing. A guarded tool used as abatchstep needs the batch itself confirmed, and both the daemon and the Lua runner enforce that independently. Every call, result, patch and executed Lua line is appended to<repoRoot>/.gmod-mcp/logs/audit.jsonl.patch_fileis locked to the repo root and refuses paths outside it.No network listener. The server transport is files inside DATA; the MCP layer is stdio. The trust boundary is the local filesystem.
run_lua— arbitrary Lua execution — lives in the optionaloptional/gmod_mcp_runluaextension, isolated becauseglua-auditforbids dynamic execution and the main bridge stays lint-clean. Development only, never on a production server.
Project config — <repoRoot>/.gmod-mcp/config.json
Every key is optional (see config.example.json):
{
"repoRoot": ".",
"addons": ["gmod_mcp_bridge"],
"clientWaitMs": 30000,
"toolAllowlist": [],
"plugins": []
}clientWaitMs is how long a client-realm call keeps retrying while the client is absent.
That realm needs a human connected, and humans crash, alt-tab and reconnect; retrying lets an
agent resume when they come back instead of failing the moment they drop. Set it to 0 to fail
fast. Server-realm calls ignore it — srcds does not come and go mid-session, so a failure there
is a real one.
Plugins
Extend the tool set with ESM modules listed in plugins. Each module exports tools:
// my_plugin.mjs
export const tools = [
{ name: "my_tool", description: "…", realm: "local", inputSchema: {}, handler: () => ({ ok: true }) },
];A failing plugin is reported on stderr without blocking startup.
Design notes
docs/2026-07-25-autonomie-client-design.md— what the client side needs before an agent can drive it without a human: name a panel instead of guessing a pixel, assert text instead of reading a JPEG, and type into a field at all. Each gap listed there blocked a real session.
Development
pnpm test # vitest
pnpm typecheck
pnpm buildLinting the GLua addon needs the host project's tools/lint.sh (four passes) plus a local
copy of GLua API definitions — see addon/gmod_mcp_bridge/.luarc.example.json.
License
MIT. See LICENSE.
Available Tools
38 toolsbatchA
Runs up to 32 server tools in ONE bridge round trip instead of one per call. A round trip costs about 0.4s, so any act-then-look sequence is dominated by transport unless it is batched. Each step reports its own ok/data/error; a step failing is data, not a transport error. IMPORTANT: with settleMs 0 every step runs in the SAME server tick, and some engine effects only land at the end of a frame -- a removed entity still reads as valid. Set settleMs (100 is usually enough) whenever a step must observe what an earlier one did. Any arg value may be {"__step": 1, "get": "index"} to read a field from an earlier step's result, so spawn-then-act still costs one round trip.
Inputs: steps (required), confirm, settleMs, stopOnError.
capture_screenA
Captures the client's screen on the next frame and returns it as a viewable image. Every byte travels in 7KB chunks paced by frame, so a full-resolution capture takes seconds and dominates any act-then-look loop: the default half scale at quality 60 is 4-6x cheaper and still legible for a Derma layout. Pass region (from read_panels' screen_x/screen_y) to capture just one panel. A capture that would exceed the client's channel budget is refused with its size rather than sent: a full-screen quality-80 capture measured 424KB and timed the client out of the server. Requires an active GMod client.
Inputs: scale, region, quality.
client_inputA
Drives the connected GMod client: movement, aim, keys, Derma clicks, typing, chat. Modal -- 'world' drives movement through CreateMove, 'ui' hands input to the panel system, and the two are mutually exclusive; the action switches mode for you. Durations are SECONDS (CreateMove runs at the client's cmdrate, so tick counts are not a duration), clamped to 5s, and everything resets after 30s or on gmod_mcp_release in the client console. To fill a form use set_text (targets a field by name and fires its change notification); type sends real keystrokes and needs a target or an already-focused field. click takes a NAMED target as well as x/y and is self-sufficient -- it moves the cursor, waits for hover to settle, presses and releases, so no move_cursor is needed first. Follow with read_view or read_panel_text to see the effect.
Inputs: action (required), x, y, up, +19 more.
force_hookA
GUARDED. Runs hook.Run(name, ...) to exercise a gamemode path without reproducing the situation. JSON cannot carry an Entity, so arguments may be tagged: {"__ent": 3}, {"__ply": "STEAM_0:1:2"}, {"__vec": [x,y,z]}, {"__ang": [p,y,r]}. Requires confirm:true.
Inputs: name (required), args, confirm.
healthA
gmod-mcp daemon status: version, detected repo root, presence of the tools/ scripts, state directory. Also probes the addon (3s) and reports the transport state -- whether this daemon owns the shared directory (a second Claude Code session starts a second daemon, which breaks every bridge tool), what is in flight, and any server handler the daemon declares but the game has not registered.
Inputs: no named parameters.
inspect_entityC
Details of one entity by index: class, model, health, owner, key-values.
Inputs: index (required).
inspect_panelA
Finds a panel by NAME, class and/or displayed text, and returns its screen rectangle, its text, whether it holds keyboard focus, and the other matches. NAME is the registered vgui name (R_CharCreate, R_UI_Button, echat.textentry) and is what you want: class is the VGUI base the panel derives from, so a kit panel's class reads Label or Panel and searching by class can never find it. Off-screen panels are excluded unless onScreen:false -- a live tree measured 1408 panels of which 5 were on screen.
Inputs: name, class, index, contains, +1 more.
lintA
Runs tools/lint.sh on an addon (name or path). Returns structured findings (file, line, rule) and the exit code. Exit 0 means clean.
Inputs: addon (required).
packageA
Builds an addon's .gma through tools/package-gma.sh, linting first and refusing on failure. Output lands in dist/.
Inputs: addon (required).
patch_fileA
Replaces a file's contents (inside the repo) after backing it up. Returns a patch id for restore_patch, plus the unified diff. rationale explains the change.
Inputs: file (required), content (required), rationale (required).
read_client_convarsC
Client-side convar values. Without names, returns a common subset.
Inputs: names.
read_consoleB
CLIENT Lua errors captured since load. GMod does not expose the console buffer itself.
Inputs: no named parameters.
read_convarsB
Server convar values. Without names, returns a common subset.
Inputs: names.
read_entitiesA
Lists entities, filterable by class. Returns index, class, model and position.
Inputs: class, limit.
read_hooksB
Registered hooks (hook.GetTable), filterable by event. Returns event -> identifiers.
Inputs: event.
read_logsA
Reads the server logs. source=game (Lua errors, -condebug) or stdout (the wrapper). sinceBoot:true bounds output to the current boot. errorsOnly:true returns structured runtime findings.
Inputs: source, sinceBoot, errorsOnly.
read_net_messagesB
Registered net messages (util.AddNetworkString) and whether a net.Receive exists.
Inputs: no named parameters.
read_panelsB
Derma/VGUI panel tree: class, name, visibility, size, position both parent-relative (x/y) and absolute (screen_x/screen_y), whether mouse input is enabled, and on_screen. Filter on on_screen: visible is the panel's own flag only, so a flat tree is mostly panels belonging to closed menus. Use screen_x/screen_y to aim a click or a capture region -- x/y are relative to the parent and are (0,0) for most nested panels.
Inputs: maxDepth.
read_panel_textA
Dumps what the interface DISPLAYS, as text: name, class, screen rectangle and text content of each panel under a named root. Use this instead of capture_screen to assert a value -- a capture travels in 7KB chunks paced one per frame, and a number read off a compressed JPEG is not an assertion. Text comes from GetText, GetValue, or a .label/.text/.title field (kit buttons paint their label and answer '' to GetText). The list is depth-first with depth relative to the root, so the parent chain is recoverable from the ordering: the DTextEntry that follows the DLabel 'Prénom' is that field. capture_screen remains the tool for anything visual -- z-order, overlap, a missing glyph.
Inputs: root, index, limit, maxDepth, +2 more.
read_playersA
Lists players: name, SteamID, team/job, ping, position, health.
Inputs: no named parameters.
read_runtimeA
Snapshot of server state: map, gamemode, CurTime, player and entity counts, uptime.
Inputs: no named parameters.
read_timersA
State of named timers: exists, time left, repetitions left. GMod cannot enumerate timers, so names is required to get anything back.
Inputs: names.
read_viewA
What the client sees and is pointed at: eye position and angles, aim trace (class, index, distance, hit position), health, weapon, cursor position and visibility, the hovered panel, the panel holding keyboard focus, and the current scripted-input mode. This is the cheap half of an act-then-look loop -- one chunk, no image -- and answers 'am I aimed at the door' without a screenshot.
Inputs: no named parameters.
reload_addonA
Touches every .lua file of an addon to trigger autorefresh. Best-effort; structural changes still need a restart.
Inputs: addon (required).
reload_fileA
Triggers GMod's Lua autorefresh for a file by bumping its mtime. Best-effort: it covers edits, while new files and autoruns still need a restart.
Inputs: file (required).
restore_patchC
Reverts a patch by id, restoring the previous state.
Inputs: id (required).
run_console_commandA
Runs a server console command (game.ConsoleCommand -- queued, around 0.25s of latency).
Inputs: command (required).
run_iterationA
One full iteration: optional patch -> reload (or a restart note) -> validate. Returns the applied patch and the verdict. This is the core of the edit/observe/fix loop.
Inputs: addon (required), file, reload, content, +1 more.
run_luaA
GUARDED. Runs arbitrary Lua server-side (RunString) and returns the resulting value. Requires confirm:true. Audited.
Inputs: code (required), confirm.
run_testA
Runs a GLua test file server-side and returns {passed, failed, results}. path is relative to lua/, e.g. 'myaddon/tests/x.lua'. The file returns a table { [name] = function(t) end }.
Inputs: path (required).
send_debugA
Prints a message server-side, useful for marking the log or tracing.
Inputs: message (required).
set_player_stateA
GUARDED, DarkRP. Sets a player's money, job, salary or RP name. Money is in INTEGER CENTS (1.50$ is 150) and goes through the r-capitalism ledger when it is loaded, so the audited invariant stays intact. Fails with a named error when DarkRP is absent. Requires confirm:true.
Inputs: target (required), job, rpname, salary, +2 more.
spawn_entityA
GUARDED. Creates and spawns an entity at a position. Returns its EntIndex, which the other world tools take as a target. Requires confirm:true.
Inputs: pos (required), class (required), ang, model, +2 more.
start_serverB
Starts the dedicated server through tools/start-server.sh [map] [gamemode] [tickrate] and records the log's boot boundary. Script defaults: rp_nycity_day/darkrp/33.
Inputs: map, gamemode, tickrate.
stop_serverB
Stops the dedicated server through tools/stop-server.sh.
Inputs: no named parameters.
sync_configA
Reapplies server-config/ and (re)creates the symlinks through tools/sync-server-config.sh. check:true compares without writing (--check).
Inputs: check.
validateC
Verdict for the loop: the addon's lint plus the current boot's runtime errors. ok=true when lint is clean AND no runtime error was seen.
Inputs: addon (required).
world_editA
GUARDED. Acts on one entity or player: remove, teleport, set_ang, freeze, unfreeze, set_health, set_armor, give, strip. target is an entity index or a player's SteamID/name. Requires confirm:true.
Inputs: action (required), target (required), ang, pos, +3 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 38 tools
Each tool targets a distinct aspect of GMod: reading entities vs inspecting vs spawning vs editing; reading panels vs inspecting vs capturing; running commands vs Lua vs tests; file patching vs linting vs validation; etc. There is no ambiguity between tools despite the large set.
The vast majority of tools follow a clear verb_noun pattern (e.g., read_entities, run_console_command, spawn_entity). A few tools like 'batch', 'health', and 'client_input' deviate slightly, but overall the naming is predictable and consistent.
With 38 tools, this is on the higher side of appropriate. While each tool serves a specific purpose in managing a GMod server, the count is above the typical 3-15 range. However, the complexity of the domain justifies the number, so it's borderline heavy rather than excessive.
The tool set covers most major operations: CRUD for entities and players, panel inspection and screen capture, server control, file management, linting, workflow automation (batch, iteration). Minor gaps exist (e.g., no direct tool for installing addons), but the surface is largely complete for development and debugging.
Related MCP Connectors
An MCP server that gives your AI access to the source code and docs of all public github repos
Official Duck Framework MCP server for AI-assisted development and project scaffolding.
Generate game-ready 3D models, textures, and audio from natural language, over MCP.
MCP server for AI dialogue using various LLM models via AceDataCloud
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceA Model Context Protocol server that gives AI assistants native access to Minecraft mod development tools — decompile, remap, search, and analyze Minecraft source code directly from your AI workflow.130 npm50MIT
- AlicenseNot gradedqualityCmaintenanceA local MCP server plus a bundled Godot editor addon that lets an AI agent create, inspect, run, debug, and export real Godot 4.6 games through tools.2PolyForm Small Business 1.0.0
- AlicenseAqualityCmaintenanceAn MCP server that empowers AI coding agents to work effectively with Minecraft mod development, providing static analysis of decompiled source code and runtime interaction with a running Minecraft instance.3168 npm18MIT
- AlicenseNot gradedqualityDmaintenanceA local MCP server that gives AI coding agents persistent memory and context across sessions.4 npmMIT