game-bridge-mcp
game-bridge-mcp
Позвольте ИИ-агенту запустить вашу игру, управлять ею и читать, что произошло — по 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от каждого экземпляра во время выполнения, поэтому отладочная команда, добавленная вами утром, доступна для вызова уже днём — без выпуска этого пакета, без переподключения и без расхождения между тем, что агент считает принимаемым игрой, и тем, что она принимает на самом деле.
Он не привязан к движку. Любой язык, что угодно, что может обслуживать четыре небольших HTTP-эндпоинта на localhost. Контракт намеренно короткий.
Быстрый старт
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 killДля launch_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. Обязательной схемы нет — это ваша игра, — но несколько общепринятых полей открывают возможности моста:
{
"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") — манифест, сериализованный из типизированного языка, обычно представляет их именно так. Мост включает их в описание, а не выводит default в схеме, потому что default: "false" для свойства boolean строгий клиент может отвергнуть. 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 выбран намеренно: несколько копий одной и той же игры могут работать одновременно, и вопрос «какая это сборка?» иначе невозможно ответить извне процесса.
Правила, которым должен следовать тот, кто пишет файл:
3. Декларация запуска
Проект объявляет, как он запускается, один раз, так что вызывающему никогда не приходится вводить команду сборки или выбирать порт. Поместите gamebridge.json в корень проекта — мост поднимается вверх от своего рабочего каталога, чтобы найти его, как это делает любой другой JS-инструмент для поиска своего конфига, а --config <файл> переопределяет его.
{
"name": "Orbital Freight",
"launch": {
"command": "./gradlew lwjgl3:run -PdebugPort={port} --console=plain",
"cwd": ".",
"portRange": "7820-7839",
"readyTimeoutMs": 180000,
"env": { "ORBITAL_DEV": "1" }
}
}Поле | Значение |
| Командная строка. Подставляется |
| Альтернатива |
| Рабочий каталог, разрешается относительно этого файла — а не относительно того места, откуда 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-схемы. |
|
| Выполняет инструмент, ждёт подтверждения от игры, возвращает итоговый дайджест. |
Рекламируются только эти семь. Собственные инструменты игры доступны через 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 --testПубликация
Пока не опубликовано в 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"files в package.json ограничивает архив 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