game-bridge-mcp
game-bridge-mcp
让 AI 代理启动你的游戏、驱动它,并读取所发生的一切——通过 HTTP,每个实例一个端口。
你的游戏已经知道关于自身的一切:屏幕上有什么、每个实体在哪里、它接受哪些命令。game-bridge-mcp 是一个 MCP 服务器,它把这些交给代理——作为代理可以调用的工具,从运行中的游戏动态发现,而不是在这里硬编码。
agent ──MCP(stdio)──▶ game-bridge-mcp ──HTTP──▶ 127.0.0.1:7820 ← it launched this one
├───────▶ 127.0.0.1:7801 ← your IDE started this one
└───────▶ 127.0.0.1:7802 ← a colleague's session有三件事使它不仅仅是一个你花一个下午就能写出来的调试桥接脚本:
它启动实例并选择端口。 调用者永远不需要选择端口或输入构建命令,因此两个代理不会冲突,桥接器会回收它启动的东西——会话结束后不会留下孤儿游戏窗口。
每个工具都接受
port。 一个桥接器驱动你运行的每一个实例:一台机器上的多个代理,或者一个代理并排比较两个构建。工具列表来自游戏。 桥接器在运行时从每个实例获取
GET /tools,因此你今天早上添加的调试命令下午就可以调用——无需发布此包、无需重新连接,代理认为游戏接受的与游戏实际接受的之间也不会产生漂移。
它与引擎无关。任何语言,任何能在 localhost 上提供四个小型 HTTP 端点的东西都可以。这个契约刻意保持简短。
快速开始
npx @wildware/game-bridge-mcp --help将其注册到 MCP 客户端——对于 Claude Code,从你的项目目录执行:
claude mcp add game-bridge -- npx -y @wildware/game-bridge-mcp然后,从代理的角度:
launch_instance {} // start a game; the bridge picks the port
list_instances {} // ...or find one already running
list_toolsets { "port": 7820 } // what can this instance do?
describe_toolset { "port": 7820, "name": "play" } // exact schemas
call_tool { "port": 7820, "name": "drop", "arguments": { "x": 1.2 } }
stop_instance { "port": 7820 } // clean shutdown, not a killlaunch_instance 需要一个启动声明。其他一切都可以针对任何实现了 HTTP 接口的游戏工作,无论这个桥接器是否启动了它。
Related MCP server: minecraft-mcp
契约
实现这个,你的游戏就可以被任何代理通过这个桥接器驱动,而这里没有任何代码需要了解它。它有三个部分,每一部分都带来特定的价值:
部分 | 它带来的价值 |
读取和驱动一个运行中的实例。 | |
无需猜测端口即可找到实例。 | |
无需人工选择端口即可启动实例。 |
只有第 1 部分是必需的。第 2 部分让发现变得可靠;第 3 部分让整个事情变得愉快。
1. HTTP 接口
在 127.0.0.1:<port> 上提供这些端点,端口来自一个显式的调试标志。只绑定到 loopback,并且除非游戏以该标志启动,否则保持整个接口关闭:这是一个调试接口,不是网络服务。
GET /health — 必需
存活性和身份检查。发现机制会对一个范围内的每个端口调用它,所以它必须廉价。
GET /health{ "ok": true, "frame": 91422 }ok: true 是将端口标记为我们的的标志。一个以其他内容响应 HTTP 的端口会被报告给代理为"其他东西占用了这个端口"——这是一个不同的问题,需要不同的修复方式,与"游戏没有运行"不同。
frame 是一个在进程生命周期内递增的计数器。桥接器监视它:frame 倒退意味着一个新进程正在这个端口上响应,缓存的工具清单会自动丢弃。这就是让重建并重新运行对代理不可见的原因。
GET /state — 必需
完整快照:代理可能想知道的一切,以 JSON 形式。没有必需的 schema——这是你的游戏——但一些约定俗成的字段可以解锁桥接器功能:
{
"frame": 91422,
"simFrame": 48110,
"completedCommandId": 17,
"paused": false,
"ui": {
"screen": "GameScreen",
"elements": [ { "label": "Restart", "visible": true } ]
},
"events": [ { "m": "merge:cherry" }, { "m": "click:Restart" } ],
"game": { "score": 1280, "state": "RUNNING" }
}字段 | 桥接器为什么关心它 |
| 重启检测;命令已运行的兜底确认。 |
| 命令已运行的强确认——见下文。 |
| 由 |
| 包含在紧凑的 |
| 最近的事件,在每条命令之后返回,以便代理看到结果。纯字符串也被接受。 |
| 标量字段包含在摘要中。嵌套对象和数组不包含——那是兆字节级实体列表所在的地方。 |
你放在这里的其他一切都会被 get_state 原样透传。
GET /command — 必需
GET /command?cmd=spawn&type=cherry&x=-1.5{ "accepted": true, "commandId": 18, "frame": 91430 }命令名称的键是 cmd,而不是 name——命令通常会自带一个 name 参数,重复的查询键会静默覆盖正在调用的命令。其他每个查询参数都是一个参数。
这个端点是即发即忘的,这是理解契约的最重要的一点。 它在命令被排队的那一刻就从 HTTP 线程返回;命令本身稍后在游戏线程上运行。紧接着读取 /state 的客户端读到的是命令发生之前的世界。测试看起来不稳定;游戏没问题。
桥接器处理了这一点,它处理的方式就是你的游戏应该支持的:
GET /command?...→ 记下返回的commandId。轮询
GET /state,直到completedCommandId >= commandId。返回那个状态——确实是在命令运行之后。
如果你的游戏不发布 completedCommandId,桥接器会降级为等待 frame 前进两次,并将结果标记为 "confirmation": "frames-advanced",这样代理就知道它得到的是较弱的保证。发布 completedCommandId 只需要几行代码,而且值得:
// game thread, once per frame
while (true) {
val cmd = queue.poll() ?: break
apply(cmd)
completedCommandId = cmd.id // published in the next /state snapshot
}强烈建议提供一个 close 命令,通过正常的关闭流程来关闭游戏:它让代理无需杀死进程就能结束一个实例。桥接器对 close 有特殊处理——它从不等待一个不可能到达的完成信号,而是等待端口安静下来。
GET /tools — 可选,但这是最精彩的部分
清单:你的游戏可以被要求做什么,用它自己的话来说。
{
"game": { "name": "Orbital Freight", "version": "0.9.2", "protocol": 1 },
"toolsets": [
{
"name": "play",
"description": "Drive the game the way a player does.",
"tools": [
{
"name": "drop",
"description": "Release the held crate, aiming first if x is given.",
"args": [
{ "name": "x", "type": "number", "description": "World x, -2..2", "required": true, "default": null },
{ "name": "settle", "type": "boolean", "description": "Wait for the stack to settle", "default": "true" }
]
}
]
}
],
"passthrough": {
"description": "Any command the debug bridge accepts, passed straight through.",
"examples": ["set_seed { seed }", "set_gravity { x, y }"]
}
}字段 | 含义 |
| 身份标识。由 |
| 为文档版本化,而不是命令集。添加命令不会改变这里;重组清单才会。它让桥接器区分"我读不懂这个"和"这个游戏知道的命令和上次不同"。当前版本: |
| 以调用者想要做什么来命名的分组,而不是按你的代码如何组织。保持少量且直观。 |
| 代理调用的名称。 |
| 为代理编写。说明它做什么以及何时使用它——这是模型推理所依据的文本。 |
|
|
| 要发送的 |
| 对于不能等待的命令为 |
| 如果你已经有 JSON Schema,发送它而不是 |
| 自由文本加示例,描述你尚未正式发布的命令。 |
默认值可以是字符串("true"、"0.05")——从类型化语言序列化的清单通常会这样渲染它们。桥接器将它们折叠到描述中,而不是在 schema 中发出 default,因为 boolean 属性上的 default: "false" 可能会被严格的客户端拒绝。null 表示"无默认值"。
解析器刻意保持宽容,因为清单是用手头已有的序列化器编写的:
toolsets可以是对象数组,也可以是name → toolset的映射。参数可以放在
args、arguments或params下,可以是对象数组、裸名称数组,也可以是name → { type, description }的映射。格式错误的工具会被丢弃,而不是致命错误。一个坏条目不能使整个实例离线。
如果 /tools 返回 404,什么也不会坏。 桥接器回退到只包含契约级工具的内置清单,并告诉代理游戏没有发布命令列表,所以通过 raw_command 工作并读取 /state。实例相应地报告为 live 或 live-no-manifest,--manifest ./my-game.json 为无法修改的游戏从文件提供清单。
2. 自我注册
端口扫描是发现机制的弱形式:受限于某人猜测的范围,在游戏应答之前对身份保持沉默,并且在启动期间容易产生假阴性——而这正是代理最可能查看的时刻。
所以一个成功绑定其调试端口的游戏会写入一个小的 JSON 文件来表明自己的身份:
~/.game-bridge/instances/<pid>.json{
"name": "Orbital Freight",
"version": "0.9.2",
"protocol": 1,
"port": 7820,
"pid": 12345,
"host": "127.0.0.1",
"started": "2026-08-20T22:27:19.774Z",
"cwd": "/home/dev/checkouts/main"
}cwd 是刻意的:同一个游戏的多个检出同时运行,"这是哪个构建?"否则从进程外部无法回答。
写入者必须遵守的规则:
在端口绑定之后再写入条目,绝不提前。为从未被占用的端口写条目比不写更糟。
在干净关闭时删除条目。
绝不让注册表故障破坏游戏。 目录不可写、主目录只读、沙箱环境——游戏都必须照常启动并照常提供端点服务。这是广告,不是基础设施。
若设置了
GAME_BRIDGE_INSTANCES(条目目录)或GAME_BRIDGE_HOME(其父目录),则予以遵循。
读者必须遵守的规则——这些更重要:
条目仅供参考,绝不具有权威性。 崩溃或强制终止都会留下文件。这在实践中屡见不鲜。
在相信任何条目之前,先用
GET /health验证。 端口无响应的条目是过期文件,不是运行中的游戏,必须如实报告为过期文件而非实例。绝不因条目而信任超过实时游戏本身。 桥接器在游戏应答时从
/tools获取名称和版本,条目仅用于补充线上协议无法传达的信息:pid、工作目录、启动时间。默认不删除其他进程的文件。 仍在绑定端口的游戏与已崩溃的游戏在一两秒内无法区分。桥接器仅在显式指定
prune: true时清理,且仅在确认端口已死之后。持续扫描。 早于注册表出现的游戏仍然存在;桥接器合并注册表条目与端口扫描结果,按端口去重,并将每个实例的
discovery报告为registry、scan或both。
3. 启动声明
项目只需声明一次启动方式,调用方就永远不必输入构建命令或挑选端口。将 gamebridge.json 放在项目根目录——桥接器会从其工作目录向上查找该文件,方式与其他所有 JS 工具查找配置的方式相同,--config <file> 可覆盖此行为。
{
"name": "Orbital Freight",
"launch": {
"command": "./gradlew lwjgl3:run -PdebugPort={port} --console=plain",
"cwd": ".",
"portRange": "7820-7839",
"readyTimeoutMs": 180000,
"env": { "ORBITAL_DEV": "1" }
}
}字段 | 含义 |
| Shell 命令行。 |
|
|
| 工作目录,相对于此文件解析——而非相对于 MCP 客户端恰好启动桥接器的位置,后者几乎从不等于项目目录。 |
| 启动器可占用的端口范围。默认 |
| 等待 |
| 额外环境变量和尾部参数。 |
启动器随后保证:
端口经过两次验证为空闲——既无绑定,也无健康检查应答——因为启动中的游戏可能已在关键意义上占用了端口,而毫秒前绑定测试却仍然失败。若声明的范围已满,则回退到操作系统分配的端口。
launch_instance仅在/health应答后才返回,调用方因此永远不需要编写重试循环。启动失败会响亮地失败,并附上子进程自身的输出。 游戏启动失败时,堆栈跟踪就是全部答案:
BridgeUsageError: Launch failed on port 7820: the process exited with code 1. Command: ./gradlew lwjgl3:run -PdebugPort=7820 --console=plain Working directory: /home/dev/orbital Full log: /tmp/game-bridge-logs/instance-7820-1787264781573.log Last output: 'gradlew' is not recognized as an internal or external command, operable program or batch file.stdout 和 stderr 被捕获到该日志文件中,贯穿实例整个生命周期,最近 200 行保存在内存中供
instance_log使用。子进程会被回收。 在
stop_instance时、服务器关闭时、收到 SIGINT、SIGTERM 或客户端断开连接时,每个已启动的实例都会被关闭——先执行游戏自身的close命令,若其不肯退出则终止整个进程树。已关闭的会话绝不会在桌面上留下游戏窗口。
超出干净关闭的升级措施仅适用于本桥接器启动且仍在跟踪的进程。对任何其他端口调用 stop_instance 会被拒绝,并提示你请游戏自行关闭。
工具
工具 | 参数 | 功能 |
|
| 启动游戏,挑选空闲端口,等待 |
|
| 注册表加端口扫描。名称、版本、pid、工作目录、屏幕。只读。 |
|
| 干净关闭,然后升级处理——仅适用于本桥接器启动的实例。 |
|
| 已启动实例的捕获 stdout/stderr。 |
|
| 该实例的工具集,按其自身描述。 |
|
| 完整 JSON Schema。 |
|
| 运行工具,等待游戏确认,返回结果摘要。 |
只公布这七个。游戏自身的工具通过 call_tool 访问,因为 MCP 客户端在连接时只会收到一次工具列表,之后不会再询问——固定列表对仍在开发中的游戏来说会过时,而当同一会话驱动两个不同构建时则根本是错误的。(--eager 会为无法遍历发现路径的客户端预先扁平化所有内容。)
桥接器自身的工具集
为每个合规游戏提供,无论其为何物:
工具 | 功能 |
| 完整的 |
| 存活状态和帧计数器。 |
| 按名称执行任意命令,无论是否已发布。 |
| 轮询 |
| 干净关闭;端口静默即为确认。 |
call_tool 如何解析名称
桥接器的复合工具,包括宿主应用注册的任何复合工具。复合工具会遮蔽同名游戏命令,这始终是改进而非意外:复合工具之所以使用该名称,正是因为原始命令在它所请求的事情发生之前就已返回。
游戏的清单——未命中时重新获取一次,以便重建后带有新命令的游戏能在会话中途被发现。
透传——其他任何内容都作为原始命令发送。存在于游戏中但不在清单中的命令今天仍然可用。若游戏将其拒绝为未知命令,你会收到它可接受的命令列表。
port 如何解析
每个工具都接受可选的 port(在顶层或 arguments 内)。解析顺序如下:
调用中显式的
port,命令行中的
--port,环境变量
GAME_BRIDGE_PORT,7777。
因此单实例设置永远不需要考虑端口,多实例会话也永远不需要第二个服务器。
同时驱动两个实例
这是本工具为之构建的场景:同一游戏的两个构建并排运行,一个代理,一个会话。
// 1. What is already running?
list_instances {}{
"registryDir": "/home/dev/.game-bridge/instances",
"live": [
{ "port": 7801, "discovery": "scan", "status": "live-no-manifest",
"frame": 2453, "manifest": "fallback", "screen": "GameScreen" },
{ "port": 7820, "discovery": "both", "status": "live", "game": "Orbital Freight",
"version": "0.9.2", "protocol": 1, "manifest": "game", "pid": 87488,
"cwd": "/home/dev/checkouts/main", "screen": "MenuScreen",
"toolsets": ["play", "build", "flow", "bridge"], "launchedByThisBridge": true }
],
"stale": [],
"notAGame": [],
"free": [7777, 7802, 7803]
}端口 7801 是较旧的构建,没有 /tools:仍然完全可以驱动,只是不能自我描述。端口 7820 是本桥接器启动的。
// 2. Start a second one. You do not choose the port.
launch_instance {}{ "port": 7821, "pid": 90114, "name": "Orbital Freight", "version": "0.9.3-rc1",
"cwd": "/home/dev/checkouts/rc", "readyInMs": 4080,
"logFile": "/tmp/game-bridge-logs/instance-7821-1787264835950.log" }// 3. Same seed, same move, both runs.
call_tool { "port": 7820, "name": "set_seed", "arguments": { "seed": 12345 } }
call_tool { "port": 7821, "name": "set_seed", "arguments": { "seed": 12345 } }
call_tool { "port": 7820, "name": "drop", "arguments": { "x": 1.2 } }
call_tool { "port": 7821, "name": "drop", "arguments": { "x": 1.2 } }每个都返回命令应用之后的状态,因此两者可以直接比较:
{
"port": 7821, "tool": "drop", "via": "manifest", "command": "drop",
"applied": true, "commandId": 18, "confirmation": "completedCommandId",
"frame": 948, "screen": "GameScreen",
"game": { "score": 1280, "state": "RUNNING" },
"events": ["merge:cherry", "score:+40"]
}// 4. Wait for something the command could not report.
call_tool { "port": 7821, "name": "wait_for",
"arguments": { "path": "game.pendingMerges", "equals": 0, "timeoutMs": 5000 } }
// 5. Clean up what you started. 7801 is not yours - leave it alone.
stop_instance { "port": 7821 }{ "port": 7821, "stopped": true, "how": "closed cleanly" }当出现问题时
桥接器能区分从外部看起来完全相同的失败:
GameOffline: No game is answering on http://127.0.0.1:7809.
Start one with:
./gradlew lwjgl3:run -PdebugPort=7809
NotAGameSurface: Something is listening on http://127.0.0.1:7802, but it is not a
debuggable game: GET /health returned HTTP 404.
A drivable game must answer GET /health with {"ok":true,"frame":N}. Check whether
another process has taken this port.
CommandTimeout: Command 'restart' was queued on port 7801 but was not applied
within 5000ms. The game accepted it, so it is probably blocked, frozen, or on a
screen that ignores this command.用 --launch-hint "make run PORT={port}" 设置第一条消息中指定的命令(或让 launch_instance 代为启动)。
CLI
npx @wildware/game-bridge-mcp [options]
-p, --port <n> Default port for tools that do not name one (default 7777)
--scan-range <spec> Ports list_instances sweeps (default 7777,7800-7810)
--no-scan Discover only via the instance registry
--no-registry Discover only by scanning ports
--registry-dir <dir> Where instance entries live (default ~/.game-bridge/instances)
--config <file> Project launch declaration (default: nearest gamebridge.json)
--launch-hint <cmd> Command shown when a port is dead; {port} is substituted
--manifest <file> Tool manifest for games that do not serve GET /tools
--eager Advertise every tool flatly, for clients that cannot discover
--timeout <ms> HTTP and command timeout (default 5000)
-h, --help
-v, --version环境变量:GAME_BRIDGE_PORT、GAME_BRIDGE_SCAN_RANGE(或 GAME_BRIDGE_SCAN)、GAME_BRIDGE_LAUNCH_HINT(或 GAME_BRIDGE_LAUNCH)、GAME_BRIDGE_MANIFEST、GAME_BRIDGE_CONFIG、GAME_BRIDGE_INSTANCES、GAME_BRIDGE_HOME。
桥接器记录的所有日志都输出到 stderr;stdout 是 MCP 传输通道,上面任何多余的一行都会破坏协议流。
在自己的项目中使用
这些组件既以 CLI 形式发布,也作为导出项提供。如果你的游戏需要串联多个命令的工具——"落子,然后等待棋盘稳定,然后报告分数差"——将它们注册为复合工具,即可继承协议、启动器、发现机制和错误消息,而无需维护第二份副本。
#!/usr/bin/env node
import { parseCli, applyProjectConfig, startStdioServer } from "@wildware/game-bridge-mcp";
const { config } = parseCli(process.argv.slice(2), process.env);
await applyProjectConfig(config);
await startStdioServer(config, {
composites: [
{
name: "drop_and_settle",
description: "Drop at world x and wait until nothing is moving. The main way to play.",
only: "Orbital Freight", // never offered to a game that has no crates
args: [{ name: "x", type: "number", required: true, description: "World x" }],
async run(ctx) {
const before = await ctx.state();
await ctx.commandAndSync("drop", { x: ctx.args.x });
const settled = await ctx.call("wait_for", { path: "game.moving", equals: 0, timeoutMs: 10000 });
const after = await ctx.state();
return { settled: settled.matched, scoreDelta: after.game.score - before.game.score };
},
},
],
});复合工具会获得一个限定到单个实例的上下文——state、health、command、commandAndSync、call(任何其他工具)、manifest、summarise、sleep——因此它永远不需要考虑端口。only 指定其适用的游戏,与清单中的 game.name 匹配;声称通用的桥接器绝不能向飞行模拟器提供 drop_and_settle。
复合工具的归属规则:它要么串联多个命令,要么等待 /command 无法报告的事情。任何只是一个命令加一组参数的内容都应属于游戏自身的清单,在那里它与实现它的代码保持同步。
更底层的组件——Bridge、GameClient、Launcher、readRegistry、normaliseManifest——也已导出。Bridge 和 GameClient 接受可选的 fetchImpl,这正是测试套件在无游戏、无套接字的情况下驱动整个系统的方式。
开发
npm install
npm run build # TypeScript -> dist/
npm test # builds, then runs node --test81 个测试,均无需运行中的游戏:端口解析顺序、清单缓存及其三条失效路径、/tools 404 回退、命令/轮询/确认循环及其帧推进降级、读取含过期和畸形条目的注册表、启动器端口选择、启动失败与子进程回收,以及通过内存传输驱动的 MCP 表面本身。
发布
尚未发布到 npm。发布时:
npm version minor # keep SERVER_VERSION in src/server.ts in step
npm test # prepublishOnly runs build + test again
npm pack --dry-run # confirm dist/, README.md and LICENSE are the payload
npm publish # publishConfig.access is already "public"package.json 中的 files 将 tarball 限制为 dist/、README.md 和 LICENSE;prepare 在从 git 安装时构建,因此通过 git 安装的依赖无需签入 dist/ 即可工作。
许可证
MIT — 参见 LICENSE。
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceAn educational MCP server that exposes system tools (like IP, hostname, file operations, ping) for AI agents to execute via HTTP.381MIT
- AlicenseNot gradedqualityDmaintenanceA set of MCP servers that allow AI assistants to control a Minecraft server and client, including running commands, managing plugins, taking screenshots, and calling arbitrary API methods via reflection.10MIT
- AlicenseNot gradedqualityCmaintenanceLocal MCP server that gives AI agents 44 engine tools to build, run, and debug real 2D and 3D games through conversation.MIT
- AlicenseAqualityAmaintenanceAn 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.313913MIT
Related MCP Connectors
A MCP server built for developers enabling Git based project management with project and personal…
An MCP server that gives your AI access to the source code and docs of all public github repos
MCP server exposing the Backtest360 engine API as tools for AI agents.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/wildware-uk/game-bridge-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server