mcp-mgba 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dmang-dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # mcp-mgba
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server that exposes the [mGBA](https://mgba.io) Game Boy Advance emulator to any MCP-compatible client (Claude Desktop, Claude Code, etc.).
4
+
5
+ Lets your model **read and write GBA memory, inject button presses, take screenshots, and step the emulator** — all through a clean tool interface.
6
+
7
+ ![demo](docs/demo.gif)
8
+
9
+ *Claude driving an in-development homebrew side-scroller through `mgba_press_buttons` — Start to begin, A to confirm New Game, then Right to walk and A to jump. Each frame is captured via `mgba_screenshot`.*
10
+
11
+ ## How it works
12
+
13
+ ```
14
+ +------------------+ stdio +------------------+ TCP :8765 +------------------+
15
+ | MCP client | JSON-RPC | mcp-mgba | newline JSON | mGBA emulator |
16
+ | (Claude / etc.) | ===========> | (Node.js) | ============> | bridge.lua |
17
+ +------------------+ +------------------+ +------------------+
18
+ ```
19
+
20
+ Two pieces:
21
+ - **`lua/bridge.lua`** — runs *inside* mGBA's scripting engine, opens a loopback TCP server on port 8765
22
+ - **`dist/index.js`** — Node.js MCP server, talks to the Lua bridge over TCP, exposes tools over stdio
23
+
24
+ ## Requirements
25
+
26
+ - [mGBA](https://mgba.io/downloads.html) **0.10 or newer** (with Lua scripting)
27
+ - **Node.js 18+** (for the MCP server)
28
+
29
+ ## Install
30
+
31
+ ### Option A — clone and install globally (recommended for now)
32
+
33
+ ```bash
34
+ git clone https://github.com/dmang-dev/mcp-mgba
35
+ cd mcp-mgba
36
+ npm install -g .
37
+ ```
38
+
39
+ That puts `mcp-mgba` on your `PATH` (the build runs automatically via `npm install`'s `prepare` hook). Verify with `mcp-mgba --help` (it'll print a startup line and wait for stdio — `Ctrl+C` to exit).
40
+
41
+ ### Option B — clone without global install
42
+
43
+ ```bash
44
+ git clone https://github.com/dmang-dev/mcp-mgba
45
+ cd mcp-mgba
46
+ npm install # runs the build automatically
47
+ ```
48
+
49
+ Then reference the absolute path to `dist/index.js` when registering.
50
+
51
+ ### Option C — `npx` from GitHub (no clone needed)
52
+
53
+ ```bash
54
+ npx -y github:dmang-dev/mcp-mgba
55
+ ```
56
+
57
+ `npx` will fetch, build (via `prepare`), and run the server in one shot.
58
+
59
+ ## Set up the mGBA bridge
60
+
61
+ 1. Launch mGBA and load any GBA ROM.
62
+ 2. Open **Tools > Scripting…**
63
+ 3. Click **File > Load script** and select `lua/bridge.lua` from this repo.
64
+
65
+ You should see in the scripting console:
66
+ ```
67
+ [mcp-mgba] bridge listening on 127.0.0.1:8765
68
+ [mcp-mgba] frame callback registered — bridge is active
69
+ ```
70
+
71
+ If you see a `bind failed` error, the previous instance's socket is still held — quit and relaunch mGBA.
72
+
73
+ ## Register with your MCP client
74
+
75
+ ### Claude Code (CLI)
76
+
77
+ ```bash
78
+ claude mcp add mgba --scope user mcp-mgba
79
+ ```
80
+
81
+ (if you used Option B without global install, replace `mcp-mgba` with `node /absolute/path/to/dist/index.js`)
82
+
83
+ Verify:
84
+ ```bash
85
+ claude mcp list
86
+ # mgba: mcp-mgba - ✓ Connected
87
+ ```
88
+
89
+ ### Claude Desktop
90
+
91
+ Edit `claude_desktop_config.json`:
92
+
93
+ | Platform | Path |
94
+ |---|---|
95
+ | macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
96
+ | Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
97
+ | Linux | `~/.config/Claude/claude_desktop_config.json` |
98
+
99
+ Add (assuming Option A — globally installed):
100
+ ```json
101
+ {
102
+ "mcpServers": {
103
+ "mgba": {
104
+ "command": "mcp-mgba"
105
+ }
106
+ }
107
+ }
108
+ ```
109
+
110
+ Or with explicit Node + path (Option B):
111
+ ```json
112
+ {
113
+ "mcpServers": {
114
+ "mgba": {
115
+ "command": "node",
116
+ "args": ["/absolute/path/to/mcp-mgba/dist/index.js"]
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ Restart Claude Desktop after editing.
123
+
124
+ ### Other MCP clients
125
+
126
+ The server speaks standard MCP over stdio. Run `mcp-mgba` (or `node dist/index.js`) and connect any MCP client to its stdio.
127
+
128
+ ## Configuration
129
+
130
+ | Env var | Default | Purpose |
131
+ |-------------|---------------|------------------------|
132
+ | `MGBA_HOST` | `127.0.0.1` | Bridge host to dial |
133
+ | `MGBA_PORT` | `8765` | Bridge port to dial |
134
+
135
+ ## Tools
136
+
137
+ | Tool | Description |
138
+ |------|-------------|
139
+ | `mgba_ping` | Verify bridge connectivity (returns `pong`) |
140
+ | `mgba_get_info` | Game title, code, frame count |
141
+ | `mgba_read8` / `mgba_read16` / `mgba_read32` | Read memory at an address |
142
+ | `mgba_write8` / `mgba_write16` / `mgba_write32` | Write to RAM |
143
+ | `mgba_read_range` | Read up to 4096 bytes as a byte array |
144
+ | `mgba_press_buttons` | Hold GBA buttons for N frames |
145
+ | `mgba_advance_frames` | Step the emulator N frames |
146
+ | `mgba_pause` / `mgba_unpause` | Pause / resume emulation |
147
+ | `mgba_reset` | Reset the loaded ROM |
148
+ | `mgba_screenshot` | Save a PNG of the current display |
149
+
150
+ ### GBA button names
151
+
152
+ `A`, `B`, `Select`, `Start`, `Right`, `Left`, `Up`, `Down`, `R`, `L`
153
+
154
+ ### GBA address space (cheat sheet)
155
+
156
+ | Range | Region |
157
+ |----------------|-------------------------------|
158
+ | `0x02000000` | EWRAM (256 KiB, general) |
159
+ | `0x03000000` | IWRAM (32 KiB, fast) |
160
+ | `0x04000000` | I/O registers |
161
+ | `0x05000000` | Palette RAM |
162
+ | `0x06000000` | VRAM |
163
+ | `0x07000000` | OAM |
164
+ | `0x08000000` | ROM (read-only) |
165
+
166
+ ## Troubleshooting
167
+
168
+ | Symptom | Cause / Fix |
169
+ |---|---|
170
+ | `Cannot reach mGBA bridge at 127.0.0.1:8765` | mGBA isn't running, or `bridge.lua` isn't loaded — open Tools > Scripting and load it |
171
+ | `bind failed — port 8765 may already be in use` | A previous mGBA instance still holds the socket; quit and relaunch mGBA |
172
+ | Tool calls hang | The bridge script may have errored out silently after a hot-reload — check the mGBA scripting console |
173
+ | Tools missing in Claude after install | Restart your MCP client; Claude only enumerates servers on startup |
174
+
175
+ ## Development
176
+
177
+ ```bash
178
+ npm install
179
+ npm run dev # tsc --watch — autobuilds on src/ changes
180
+ ```
181
+
182
+ The Lua side (`lua/bridge.lua` and `lua/json.lua`) needs no build step. Edit and reload via mGBA's `File > Load script`.
183
+
184
+ ## License
185
+
186
+ [MIT](LICENSE)
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { MgbaClient } from "./mgba.js";
5
+ import { registerTools } from "./tools.js";
6
+ const HOST = process.env.MGBA_HOST ?? "127.0.0.1";
7
+ const PORT = parseInt(process.env.MGBA_PORT ?? "8765", 10);
8
+ async function main() {
9
+ const mgba = new MgbaClient(HOST, PORT);
10
+ // Connect eagerly — if mGBA isn't running the server still starts, but
11
+ // each tool call will return a clear "not connected" error rather than
12
+ // crashing the MCP host.
13
+ try {
14
+ await mgba.connect();
15
+ process.stderr.write(`[mcp-mgba] connected to mGBA bridge at ${HOST}:${PORT}\n`);
16
+ }
17
+ catch (err) {
18
+ process.stderr.write(`[mcp-mgba] WARNING: could not connect to mGBA bridge (${HOST}:${PORT}): ${err}\n` +
19
+ ` Start mGBA, load a ROM, then open Tools > Scripting and run lua/bridge.lua.\n`);
20
+ }
21
+ const server = new Server({ name: "mcp-mgba", version: "0.1.0" }, { capabilities: { tools: {} } });
22
+ registerTools(server, mgba);
23
+ const transport = new StdioServerTransport();
24
+ await server.connect(transport);
25
+ process.stderr.write("[mcp-mgba] MCP server ready (stdio)\n");
26
+ }
27
+ main().catch((err) => {
28
+ process.stderr.write(`[mcp-mgba] fatal: ${err}\n`);
29
+ process.exit(1);
30
+ });
31
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,WAAW,CAAC;AAClD,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,MAAM,EAAE,EAAE,CAAC,CAAC;AAE3D,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAExC,uEAAuE;IACvE,uEAAuE;IACvE,yBAAyB;IACzB,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACrB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,0CAA0C,IAAI,IAAI,IAAI,IAAI,CAAC,CAAC;IACnF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,yDAAyD,IAAI,IAAI,IAAI,MAAM,GAAG,IAAI;YAClF,0FAA0F,CAC3F,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,EACtC,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAC;IAEF,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAE5B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;AAChE,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;IACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
package/dist/mgba.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ export interface RpcRequest {
2
+ id: number;
3
+ method: string;
4
+ params?: Record<string, unknown>;
5
+ }
6
+ export interface RpcResponse {
7
+ id: number | null;
8
+ result?: unknown;
9
+ error?: {
10
+ code: number;
11
+ message: string;
12
+ };
13
+ }
14
+ export declare class MgbaClient {
15
+ private readonly host;
16
+ private readonly port;
17
+ private socket;
18
+ private pending;
19
+ private nextId;
20
+ private buf;
21
+ private connectPromise;
22
+ constructor(host?: string, port?: number);
23
+ connect(): Promise<void>;
24
+ disconnect(): void;
25
+ get connected(): boolean;
26
+ call<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
27
+ }
28
+ //# sourceMappingURL=mgba.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mgba.d.ts","sourceRoot":"","sources":["../src/mgba.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED,qBAAa,UAAU;IAQnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IARvB,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,OAAO,CAAiD;IAChE,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,cAAc,CAA8B;gBAGjC,IAAI,GAAE,MAAoB,EAC1B,IAAI,GAAE,MAAa;IAGtC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAoDxB,UAAU,IAAI,IAAI;IAMlB,IAAI,SAAS,IAAI,OAAO,CAEvB;IAEK,IAAI,CAAC,CAAC,GAAG,OAAO,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,OAAO,CAAC,CAAC,CAAC;CAwCd"}
package/dist/mgba.js ADDED
@@ -0,0 +1,110 @@
1
+ import net from "net";
2
+ export class MgbaClient {
3
+ host;
4
+ port;
5
+ socket = null;
6
+ pending = new Map();
7
+ nextId = 1;
8
+ buf = "";
9
+ connectPromise = null;
10
+ constructor(host = "127.0.0.1", port = 8765) {
11
+ this.host = host;
12
+ this.port = port;
13
+ }
14
+ connect() {
15
+ if (this.connectPromise)
16
+ return this.connectPromise;
17
+ this.connectPromise = new Promise((resolve, reject) => {
18
+ const sock = net.createConnection({ host: this.host, port: this.port });
19
+ sock.setEncoding("utf8");
20
+ sock.once("connect", () => {
21
+ this.socket = sock;
22
+ resolve();
23
+ });
24
+ sock.once("error", (err) => {
25
+ this.connectPromise = null;
26
+ reject(err);
27
+ });
28
+ sock.on("data", (chunk) => {
29
+ this.buf += chunk;
30
+ let nl;
31
+ while ((nl = this.buf.indexOf("\n")) !== -1) {
32
+ const line = this.buf.slice(0, nl).trim();
33
+ this.buf = this.buf.slice(nl + 1);
34
+ if (line.length === 0)
35
+ continue;
36
+ let resp;
37
+ try {
38
+ resp = JSON.parse(line);
39
+ }
40
+ catch {
41
+ continue;
42
+ }
43
+ if (resp.id != null) {
44
+ const cb = this.pending.get(resp.id);
45
+ if (cb) {
46
+ this.pending.delete(resp.id);
47
+ cb(resp);
48
+ }
49
+ }
50
+ }
51
+ });
52
+ sock.on("close", () => {
53
+ this.socket = null;
54
+ this.connectPromise = null;
55
+ // Reject all in-flight calls
56
+ for (const cb of this.pending.values()) {
57
+ cb({ id: null, error: { code: -1, message: "connection closed" } });
58
+ }
59
+ this.pending.clear();
60
+ });
61
+ });
62
+ return this.connectPromise;
63
+ }
64
+ disconnect() {
65
+ this.socket?.destroy();
66
+ this.socket = null;
67
+ this.connectPromise = null;
68
+ }
69
+ get connected() {
70
+ return this.socket !== null && !this.socket.destroyed;
71
+ }
72
+ async call(method, params) {
73
+ // Lazy (re)connect — bridge.lua reloads kill the socket, and the user
74
+ // shouldn't have to restart the MCP host every time they edit the script.
75
+ if (!this.socket || this.socket.destroyed) {
76
+ try {
77
+ await this.connect();
78
+ }
79
+ catch (err) {
80
+ throw new Error(`Cannot reach mGBA bridge at ${this.host}:${this.port}. ` +
81
+ `Make sure mGBA is running with bridge.lua loaded (Tools > Scripting). ` +
82
+ `Underlying error: ${err.message}`);
83
+ }
84
+ }
85
+ return new Promise((resolve, reject) => {
86
+ const sock = this.socket;
87
+ if (!sock) {
88
+ reject(new Error("socket vanished after connect"));
89
+ return;
90
+ }
91
+ const id = this.nextId++;
92
+ this.pending.set(id, (resp) => {
93
+ if (resp.error) {
94
+ reject(new Error(`mGBA RPC error [${resp.error.code}]: ${resp.error.message}`));
95
+ }
96
+ else {
97
+ resolve(resp.result);
98
+ }
99
+ });
100
+ const msg = JSON.stringify({ id, method, params: params ?? {} }) + "\n";
101
+ sock.write(msg, (err) => {
102
+ if (err) {
103
+ this.pending.delete(id);
104
+ reject(err);
105
+ }
106
+ });
107
+ });
108
+ }
109
+ }
110
+ //# sourceMappingURL=mgba.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mgba.js","sourceRoot":"","sources":["../src/mgba.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { MgbaClient } from "./mgba.js";
3
+ export declare function registerTools(server: Server, mgba: MgbaClient): void;
4
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AA0LvC,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CA+FpE"}
package/dist/tools.js ADDED
@@ -0,0 +1,258 @@
1
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
2
+ // GBA memory map landmarks (useful in tool descriptions)
3
+ const GBA_REGIONS = `
4
+ GBA address space:
5
+ 0x02000000 EWRAM (256 KiB, general-purpose)
6
+ 0x03000000 IWRAM (32 KiB, fast stack/variables)
7
+ 0x04000000 IO registers
8
+ 0x05000000 Palette RAM (1 KiB)
9
+ 0x06000000 VRAM (96 KiB)
10
+ 0x07000000 OAM (1 KiB)
11
+ 0x08000000 ROM (up to 32 MiB, read-only)`.trim();
12
+ const VALID_KEYS = ["A", "B", "Select", "Start", "Right", "Left", "Up", "Down", "R", "L"];
13
+ // ── Tool definitions ─────────────────────────────────────────────────────────
14
+ const TOOLS = [
15
+ {
16
+ name: "mgba_ping",
17
+ description: "Check connectivity to the mGBA bridge. Returns 'pong' if the emulator is running and the Lua bridge is loaded.",
18
+ inputSchema: { type: "object", properties: {} },
19
+ },
20
+ {
21
+ name: "mgba_get_info",
22
+ description: "Get the currently-loaded game title, game code (e.g. AGBE), and frame count.",
23
+ inputSchema: { type: "object", properties: {} },
24
+ },
25
+ {
26
+ name: "mgba_read8",
27
+ description: `Read a single unsigned byte (u8) from a GBA memory address.\n\n${GBA_REGIONS}`,
28
+ inputSchema: {
29
+ type: "object",
30
+ required: ["address"],
31
+ properties: {
32
+ address: {
33
+ type: "integer",
34
+ description: "GBA memory address (decimal or hex — use 0x prefix in JSON strings, or pass as decimal integer)",
35
+ },
36
+ },
37
+ },
38
+ },
39
+ {
40
+ name: "mgba_read16",
41
+ description: "Read an unsigned 16-bit little-endian value from a GBA memory address. Address should be 2-byte aligned.",
42
+ inputSchema: {
43
+ type: "object",
44
+ required: ["address"],
45
+ properties: {
46
+ address: { type: "integer", description: "GBA memory address (must be 2-byte aligned)" },
47
+ },
48
+ },
49
+ },
50
+ {
51
+ name: "mgba_read32",
52
+ description: "Read an unsigned 32-bit little-endian value from a GBA memory address. Address should be 4-byte aligned.",
53
+ inputSchema: {
54
+ type: "object",
55
+ required: ["address"],
56
+ properties: {
57
+ address: { type: "integer", description: "GBA memory address (must be 4-byte aligned)" },
58
+ },
59
+ },
60
+ },
61
+ {
62
+ name: "mgba_write8",
63
+ description: "Write a single byte value to a GBA memory address. Only works on RAM regions (EWRAM, IWRAM). Writing to ROM has no effect.",
64
+ inputSchema: {
65
+ type: "object",
66
+ required: ["address", "value"],
67
+ properties: {
68
+ address: { type: "integer", description: "GBA RAM address" },
69
+ value: { type: "integer", minimum: 0, maximum: 255, description: "Byte value (0–255)" },
70
+ },
71
+ },
72
+ },
73
+ {
74
+ name: "mgba_write16",
75
+ description: "Write a 16-bit value to a GBA memory address (little-endian). Address must be 2-byte aligned.",
76
+ inputSchema: {
77
+ type: "object",
78
+ required: ["address", "value"],
79
+ properties: {
80
+ address: { type: "integer", description: "GBA RAM address (2-byte aligned)" },
81
+ value: { type: "integer", minimum: 0, maximum: 65535, description: "16-bit value (0–65535)" },
82
+ },
83
+ },
84
+ },
85
+ {
86
+ name: "mgba_write32",
87
+ description: "Write a 32-bit value to a GBA memory address (little-endian). Address must be 4-byte aligned.",
88
+ inputSchema: {
89
+ type: "object",
90
+ required: ["address", "value"],
91
+ properties: {
92
+ address: { type: "integer", description: "GBA RAM address (4-byte aligned)" },
93
+ value: { type: "integer", minimum: 0, description: "32-bit value" },
94
+ },
95
+ },
96
+ },
97
+ {
98
+ name: "mgba_read_range",
99
+ description: "Read a contiguous range of bytes from GBA memory and return them as an array of integers. Maximum 4096 bytes per call.",
100
+ inputSchema: {
101
+ type: "object",
102
+ required: ["address", "length"],
103
+ properties: {
104
+ address: { type: "integer", description: "Start address" },
105
+ length: { type: "integer", minimum: 1, maximum: 4096, description: "Number of bytes to read" },
106
+ },
107
+ },
108
+ },
109
+ {
110
+ name: "mgba_press_buttons",
111
+ description: `Press one or more GBA buttons for a given number of frames. Valid button names: ${VALID_KEYS.join(", ")}.`,
112
+ inputSchema: {
113
+ type: "object",
114
+ required: ["buttons"],
115
+ properties: {
116
+ buttons: {
117
+ type: "array",
118
+ items: { type: "string", enum: VALID_KEYS },
119
+ description: "List of button names to hold simultaneously",
120
+ },
121
+ frames: {
122
+ type: "integer",
123
+ minimum: 1,
124
+ default: 1,
125
+ description: "Number of frames to hold the buttons (at 60 fps; default 1)",
126
+ },
127
+ },
128
+ },
129
+ },
130
+ {
131
+ name: "mgba_advance_frames",
132
+ description: "Advance emulation by N frames without returning to the event loop. Useful for precise timing in tests.",
133
+ inputSchema: {
134
+ type: "object",
135
+ properties: {
136
+ count: { type: "integer", minimum: 1, default: 1, description: "Number of frames to advance (default 1)" },
137
+ },
138
+ },
139
+ },
140
+ {
141
+ name: "mgba_pause",
142
+ description: "Pause emulation.",
143
+ inputSchema: { type: "object", properties: {} },
144
+ },
145
+ {
146
+ name: "mgba_unpause",
147
+ description: "Resume emulation after a pause.",
148
+ inputSchema: { type: "object", properties: {} },
149
+ },
150
+ {
151
+ name: "mgba_reset",
152
+ description: "Reset the currently-loaded ROM (equivalent to pressing the GBA reset button).",
153
+ inputSchema: { type: "object", properties: {} },
154
+ },
155
+ {
156
+ name: "mgba_screenshot",
157
+ description: "Take a screenshot of the current GBA display and save it to a file. Returns the saved file path.",
158
+ inputSchema: {
159
+ type: "object",
160
+ properties: {
161
+ path: {
162
+ type: "string",
163
+ description: "Absolute file path to save the PNG (optional — defaults to a temp file)",
164
+ },
165
+ },
166
+ },
167
+ },
168
+ ];
169
+ // ── Helpers ──────────────────────────────────────────────────────────────────
170
+ function ok(text) {
171
+ return { content: [{ type: "text", text }] };
172
+ }
173
+ function formatHex(n) {
174
+ if (typeof n !== "number")
175
+ return String(n);
176
+ return `${n} (0x${n.toString(16).toUpperCase()})`;
177
+ }
178
+ // ── Registration ─────────────────────────────────────────────────────────────
179
+ export function registerTools(server, mgba) {
180
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
181
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
182
+ const { name, arguments: args = {} } = req.params;
183
+ const p = args;
184
+ switch (name) {
185
+ case "mgba_ping": {
186
+ const r = await mgba.call("ping");
187
+ return ok(r);
188
+ }
189
+ case "mgba_get_info": {
190
+ const r = await mgba.call("get_info");
191
+ return ok(`Title: ${r.title}\nCode: ${r.code}\nFrame: ${r.frame}`);
192
+ }
193
+ case "mgba_read8": {
194
+ const v = await mgba.call("read8", { address: p.address });
195
+ return ok(`0x${p.address.toString(16).toUpperCase()}: ${formatHex(v)}`);
196
+ }
197
+ case "mgba_read16": {
198
+ const v = await mgba.call("read16", { address: p.address });
199
+ return ok(`0x${p.address.toString(16).toUpperCase()}: ${formatHex(v)}`);
200
+ }
201
+ case "mgba_read32": {
202
+ const v = await mgba.call("read32", { address: p.address });
203
+ return ok(`0x${p.address.toString(16).toUpperCase()}: ${formatHex(v)}`);
204
+ }
205
+ case "mgba_write8": {
206
+ await mgba.call("write8", { address: p.address, value: p.value });
207
+ return ok(`Wrote ${formatHex(p.value)} → 0x${p.address.toString(16).toUpperCase()}`);
208
+ }
209
+ case "mgba_write16": {
210
+ await mgba.call("write16", { address: p.address, value: p.value });
211
+ return ok(`Wrote ${formatHex(p.value)} → 0x${p.address.toString(16).toUpperCase()}`);
212
+ }
213
+ case "mgba_write32": {
214
+ await mgba.call("write32", { address: p.address, value: p.value });
215
+ return ok(`Wrote ${formatHex(p.value)} → 0x${p.address.toString(16).toUpperCase()}`);
216
+ }
217
+ case "mgba_read_range": {
218
+ const bytes = await mgba.call("read_range", {
219
+ address: p.address,
220
+ length: p.length,
221
+ });
222
+ const hex = bytes
223
+ .map((b) => b.toString(16).padStart(2, "0").toUpperCase())
224
+ .join(" ");
225
+ const addr = p.address.toString(16).toUpperCase();
226
+ return ok(`0x${addr} [${bytes.length} bytes]:\n${hex}`);
227
+ }
228
+ case "mgba_press_buttons": {
229
+ await mgba.call("press_buttons", { buttons: p.buttons, frames: p.frames ?? 1 });
230
+ const keys = p.buttons.join("+");
231
+ return ok(`Pressed ${keys} for ${p.frames ?? 1} frame(s)`);
232
+ }
233
+ case "mgba_advance_frames": {
234
+ const frame = await mgba.call("advance_frames", { count: p.count ?? 1 });
235
+ return ok(`Advanced ${p.count ?? 1} frame(s). Current frame: ${frame}`);
236
+ }
237
+ case "mgba_pause": {
238
+ await mgba.call("pause");
239
+ return ok("Emulation paused");
240
+ }
241
+ case "mgba_unpause": {
242
+ await mgba.call("unpause");
243
+ return ok("Emulation resumed");
244
+ }
245
+ case "mgba_reset": {
246
+ await mgba.call("reset");
247
+ return ok("ROM reset");
248
+ }
249
+ case "mgba_screenshot": {
250
+ const path = await mgba.call("screenshot", p.path ? { path: p.path } : {});
251
+ return ok(`Screenshot saved: ${path}`);
252
+ }
253
+ default:
254
+ throw new Error(`Unknown tool: ${name}`);
255
+ }
256
+ });
257
+ }
258
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
package/docs/demo.gif ADDED
Binary file
package/lua/bridge.lua ADDED
@@ -0,0 +1,246 @@
1
+ -- bridge.lua: mGBA scripting bridge for mcp-mgba
2
+ --
3
+ -- Exposes a newline-delimited JSON-RPC server on 127.0.0.1:8765.
4
+ -- Load via mGBA: Tools > Scripting... > Open Script (select this file).
5
+ --
6
+ -- json.lua must live in the same folder as this file.
7
+ -- socket is a pre-registered global in mGBA's Lua environment.
8
+ --
9
+ -- mGBA socket API (discovered via metatable probe):
10
+ -- bind, listen, accept, connect, send, receive, hasdata, poll, _hook
11
+ --
12
+ -- Requires mGBA >= 0.10.
13
+
14
+ local json = require("json")
15
+
16
+ local HOST = "127.0.0.1"
17
+ local PORT = 8765
18
+
19
+ -- ── GBA key name → bitmask bit index ────────────────────────────────────────
20
+ local KEY_BIT = {
21
+ A = 0, B = 1, Select = 2, Start = 3,
22
+ Right = 4, Left = 5, Up = 6, Down = 7,
23
+ R = 8, L = 9,
24
+ }
25
+
26
+ -- ── Per-frame key-hold state ─────────────────────────────────────────────────
27
+ local hold_bits = 0
28
+ local hold_frames = 0
29
+
30
+ -- ── Command handlers ─────────────────────────────────────────────────────────
31
+
32
+ local function cmd_ping() return "pong" end
33
+ local function cmd_get_info()
34
+ return { title = emu:getGameTitle(), code = emu:getGameCode(), frame = emu:currentFrame() }
35
+ end
36
+
37
+ -- emu:read8/16/32 are flaky when called repeatedly via pcall from the frame
38
+ -- callback ("invoking failed" intermittently). emu:readRange is reliable, so
39
+ -- we route the typed reads through it and decode little-endian on the Lua side.
40
+ local function cmd_read8(p)
41
+ local raw = emu:readRange(assert(p.address, "address required"), 1)
42
+ return raw:byte(1)
43
+ end
44
+ local function cmd_read16(p)
45
+ local raw = emu:readRange(assert(p.address, "address required"), 2)
46
+ return raw:byte(1) | (raw:byte(2) << 8)
47
+ end
48
+ local function cmd_read32(p)
49
+ local raw = emu:readRange(assert(p.address, "address required"), 4)
50
+ return raw:byte(1) | (raw:byte(2) << 8) | (raw:byte(3) << 16) | (raw:byte(4) << 24)
51
+ end
52
+
53
+ -- emu:writeN — like emu:readN — intermittently throws "invoking failed" when
54
+ -- pcall'd from a frame callback. Retry up to a few times before giving up.
55
+ local function retry_call(fn, ...)
56
+ local attempts = 8
57
+ local last_err
58
+ for _ = 1, attempts do
59
+ local ok, err = pcall(fn, ...)
60
+ if ok then return true end
61
+ last_err = err
62
+ end
63
+ error(last_err)
64
+ end
65
+
66
+ local function cmd_write8(p)
67
+ local addr = assert(p.address, "address required")
68
+ local val = assert(p.value, "value required")
69
+ retry_call(function() emu:write8(addr, val) end)
70
+ return true
71
+ end
72
+ local function cmd_write16(p)
73
+ local addr = assert(p.address, "address required")
74
+ local val = assert(p.value, "value required")
75
+ retry_call(function() emu:write16(addr, val) end)
76
+ return true
77
+ end
78
+ local function cmd_write32(p)
79
+ local addr = assert(p.address, "address required")
80
+ local val = assert(p.value, "value required")
81
+ retry_call(function() emu:write32(addr, val) end)
82
+ return true
83
+ end
84
+
85
+ local function cmd_read_range(p)
86
+ local addr = assert(p.address, "address required")
87
+ local len = assert(p.length, "length required")
88
+ if len > 4096 then error("length exceeds 4096 byte limit") end
89
+ local raw = emu:readRange(addr, len)
90
+ local bytes = {}
91
+ for i = 1, #raw do bytes[i] = raw:byte(i) end
92
+ return bytes
93
+ end
94
+
95
+ local function cmd_press_buttons(p)
96
+ local keys = assert(p.buttons, "buttons required")
97
+ local bits = 0
98
+ for _, name in ipairs(keys) do
99
+ local b = KEY_BIT[name]
100
+ if not b then error("unknown key: " .. tostring(name)) end
101
+ bits = bits | (1 << b)
102
+ end
103
+ hold_bits = bits
104
+ hold_frames = p.frames or 1
105
+ return true
106
+ end
107
+
108
+ local function cmd_advance_frames(p)
109
+ local n = p.count or 1
110
+ for _ = 1, n do emu:frameAdvance() end
111
+ return emu:currentFrame()
112
+ end
113
+
114
+ local function cmd_pause() emu:pause(); return true end
115
+ local function cmd_unpause() emu:unpause(); return true end
116
+ local function cmd_reset() emu:reset(); return true end
117
+
118
+ local function cmd_screenshot(p)
119
+ local path = p.path or (os.tmpname() .. ".png")
120
+ -- mGBA's emu:screenshot takes the destination path directly and writes
121
+ -- the PNG itself; it does not return an image object.
122
+ emu:screenshot(path)
123
+ return path
124
+ end
125
+
126
+ -- ── Dispatch table ───────────────────────────────────────────────────────────
127
+
128
+ local HANDLERS = {
129
+ ping = cmd_ping,
130
+ get_info = cmd_get_info,
131
+ read8 = cmd_read8,
132
+ read16 = cmd_read16,
133
+ read32 = cmd_read32,
134
+ write8 = cmd_write8,
135
+ write16 = cmd_write16,
136
+ write32 = cmd_write32,
137
+ read_range = cmd_read_range,
138
+ press_buttons = cmd_press_buttons,
139
+ advance_frames = cmd_advance_frames,
140
+ pause = cmd_pause,
141
+ unpause = cmd_unpause,
142
+ reset = cmd_reset,
143
+ screenshot = cmd_screenshot,
144
+ }
145
+
146
+ local function dispatch(cmd)
147
+ if not cmd.method then
148
+ return nil, { code = -32600, message = "missing method field" }
149
+ end
150
+ local handler = HANDLERS[cmd.method]
151
+ if not handler then
152
+ return nil, { code = -32601, message = "unknown method: " .. cmd.method }
153
+ end
154
+ local ok, result = pcall(handler, cmd.params or {})
155
+ if not ok then
156
+ return nil, { code = -32603, message = tostring(result) }
157
+ end
158
+ return result, nil
159
+ end
160
+
161
+ -- ── Process one client's buffer — call after appending new data ──────────────
162
+
163
+ local function process_buffer(c)
164
+ while true do
165
+ local nl = c.buf:find("\n", 1, true)
166
+ if not nl then break end
167
+
168
+ local line = c.buf:sub(1, nl - 1)
169
+ c.buf = c.buf:sub(nl + 1)
170
+
171
+ if #line > 0 then
172
+ local parse_ok, cmd = pcall(json.decode, line)
173
+ local response
174
+ if parse_ok and type(cmd) == "table" then
175
+ local result, rpc_err = dispatch(cmd)
176
+ if rpc_err then
177
+ response = { id = cmd.id, error = rpc_err }
178
+ else
179
+ response = { id = cmd.id, result = result }
180
+ end
181
+ else
182
+ response = { id = nil, error = { code = -32700, message = "parse error" } }
183
+ end
184
+ c.sock:send(json.encode(response) .. "\n")
185
+ end
186
+ end
187
+ end
188
+
189
+ -- ── Server socket ────────────────────────────────────────────────────────────
190
+
191
+ local server = assert(socket.tcp(), "socket.tcp() failed")
192
+ assert(server:bind(HOST, PORT), "bind failed — port " .. PORT .. " may already be in use")
193
+ assert(server:listen(), "listen failed")
194
+
195
+ -- Active client table: array of { sock, buf }
196
+ local clients = {}
197
+
198
+ -- ── Per-frame callback ───────────────────────────────────────────────────────
199
+
200
+ callbacks:add("frame", function()
201
+
202
+ -- Key hold
203
+ if hold_frames > 0 then
204
+ emu:setKeys(hold_bits)
205
+ hold_frames = hold_frames - 1
206
+ if hold_frames == 0 then emu:setKeys(0) end
207
+ end
208
+
209
+ -- poll() flushes the socket's internal event queue. Without it, accept()
210
+ -- and hasdata() see stale state and never observe new I/O.
211
+ server:poll()
212
+ local client = server:accept()
213
+ if client then
214
+ console:log("[mcp-mgba] client connected")
215
+ table.insert(clients, { sock = client, buf = "" })
216
+ end
217
+
218
+ -- Service existing clients
219
+ local i = 1
220
+ while i <= #clients do
221
+ local c = clients[i]
222
+ c.sock:poll()
223
+ if c.sock:hasdata() then
224
+ -- mGBA's receive(maxBytes) reads up to maxBytes — non-blocking
225
+ -- when guarded by hasdata(). Wrap in pcall so any internal error
226
+ -- doesn't spam the console every frame.
227
+ local ok, data = pcall(function() return c.sock:receive(4096) end)
228
+ if ok and data and #data > 0 then
229
+ c.buf = c.buf .. data
230
+ process_buffer(c)
231
+ i = i + 1
232
+ elseif ok and data == nil then
233
+ console:log("[mcp-mgba] client disconnected")
234
+ table.remove(clients, i)
235
+ else
236
+ console:log("[mcp-mgba] receive error: " .. tostring(data))
237
+ table.remove(clients, i)
238
+ end
239
+ else
240
+ i = i + 1
241
+ end
242
+ end
243
+ end)
244
+
245
+ console:log(string.format("[mcp-mgba] bridge listening on %s:%d", HOST, PORT))
246
+ console:log("[mcp-mgba] frame callback registered — bridge is active")
package/lua/json.lua ADDED
@@ -0,0 +1,200 @@
1
+ -- json.lua: minimal JSON encode/decode for mGBA's Lua environment
2
+ -- Supports objects, arrays, strings, numbers, booleans, null.
3
+ -- No external dependencies.
4
+
5
+ local json = {}
6
+
7
+ -- ── Encoder ─────────────────────────────────────────────────────────────────
8
+
9
+ local escape_map = {
10
+ ['"'] = '\\"',
11
+ ['\\'] = '\\\\',
12
+ ['\n'] = '\\n',
13
+ ['\r'] = '\\r',
14
+ ['\t'] = '\\t',
15
+ }
16
+
17
+ local function encode_string(s)
18
+ return '"' .. s:gsub('["\\\n\r\t]', escape_map) .. '"'
19
+ end
20
+
21
+ local encode_value -- forward declaration
22
+
23
+ local function encode_array(t, n)
24
+ local parts = {}
25
+ for i = 1, n do parts[i] = encode_value(t[i]) end
26
+ return "[" .. table.concat(parts, ",") .. "]"
27
+ end
28
+
29
+ local function encode_object(t)
30
+ local parts = {}
31
+ for k, v in pairs(t) do
32
+ parts[#parts + 1] = encode_string(tostring(k)) .. ":" .. encode_value(v)
33
+ end
34
+ return "{" .. table.concat(parts, ",") .. "}"
35
+ end
36
+
37
+ encode_value = function(v)
38
+ local tv = type(v)
39
+ if tv == "nil" then return "null"
40
+ elseif tv == "boolean" then return tostring(v)
41
+ elseif tv == "number" then
42
+ -- integers stay integers, floats keep decimals
43
+ if v ~= v then return "null" end -- NaN guard
44
+ if v == math.huge or v == -math.huge then return "null" end
45
+ if math.floor(v) == v then return string.format("%d", v) end
46
+ return string.format("%.17g", v)
47
+ elseif tv == "string" then
48
+ return encode_string(v)
49
+ elseif tv == "table" then
50
+ -- detect array: sequential integer keys starting at 1
51
+ local n = #v
52
+ local is_arr = (n > 0)
53
+ if is_arr then
54
+ for k in pairs(v) do
55
+ if type(k) ~= "number" or k < 1 or k > n or math.floor(k) ~= k then
56
+ is_arr = false; break
57
+ end
58
+ end
59
+ else
60
+ -- empty table with no keys → emit as object {}
61
+ local has_keys = false
62
+ for _ in pairs(v) do has_keys = true; break end
63
+ if not has_keys then return "{}" end
64
+ end
65
+ return is_arr and encode_array(v, n) or encode_object(v)
66
+ end
67
+ return "null"
68
+ end
69
+
70
+ function json.encode(v)
71
+ return encode_value(v)
72
+ end
73
+
74
+ -- ── Decoder ──────────────────────────────────────────────────────────────────
75
+
76
+ local function skip_ws(s, i)
77
+ while i <= #s do
78
+ local c = s:sub(i, i)
79
+ if c == ' ' or c == '\t' or c == '\n' or c == '\r' then
80
+ i = i + 1
81
+ else
82
+ break
83
+ end
84
+ end
85
+ return i
86
+ end
87
+
88
+ local decode_value -- forward declaration
89
+
90
+ local function decode_string(s, i)
91
+ -- i points to the opening '"'
92
+ i = i + 1
93
+ local buf = {}
94
+ while i <= #s do
95
+ local c = s:sub(i, i)
96
+ if c == '"' then
97
+ return table.concat(buf), i + 1
98
+ elseif c == '\\' then
99
+ local e = s:sub(i + 1, i + 1)
100
+ if e == '"' then buf[#buf+1] = '"'; i = i + 2
101
+ elseif e == '\\' then buf[#buf+1] = '\\'; i = i + 2
102
+ elseif e == '/' then buf[#buf+1] = '/'; i = i + 2
103
+ elseif e == 'n' then buf[#buf+1] = '\n'; i = i + 2
104
+ elseif e == 'r' then buf[#buf+1] = '\r'; i = i + 2
105
+ elseif e == 't' then buf[#buf+1] = '\t'; i = i + 2
106
+ elseif e == 'b' then buf[#buf+1] = '\b'; i = i + 2
107
+ elseif e == 'f' then buf[#buf+1] = '\f'; i = i + 2
108
+ elseif e == 'u' then
109
+ -- \uXXXX — keep raw for now (ASCII subset only needed)
110
+ local hex = s:sub(i + 2, i + 5)
111
+ local cp = tonumber(hex, 16)
112
+ if cp and cp < 128 then
113
+ buf[#buf+1] = string.char(cp)
114
+ else
115
+ buf[#buf+1] = '?' -- non-ASCII placeholder
116
+ end
117
+ i = i + 6
118
+ else
119
+ buf[#buf+1] = e; i = i + 2
120
+ end
121
+ else
122
+ buf[#buf+1] = c; i = i + 1
123
+ end
124
+ end
125
+ error("json: unterminated string at " .. i)
126
+ end
127
+
128
+ local function decode_number(s, i)
129
+ -- grab the full number token
130
+ local j = i
131
+ if s:sub(j, j) == '-' then j = j + 1 end
132
+ while j <= #s and s:sub(j, j):match('%d') do j = j + 1 end
133
+ if j <= #s and s:sub(j, j) == '.' then
134
+ j = j + 1
135
+ while j <= #s and s:sub(j, j):match('%d') do j = j + 1 end
136
+ end
137
+ if j <= #s and s:sub(j, j):match('[eE]') then
138
+ j = j + 1
139
+ if j <= #s and s:sub(j, j):match('[+-]') then j = j + 1 end
140
+ while j <= #s and s:sub(j, j):match('%d') do j = j + 1 end
141
+ end
142
+ return tonumber(s:sub(i, j - 1)), j
143
+ end
144
+
145
+ local function decode_object(s, i)
146
+ local t = {}
147
+ i = skip_ws(s, i + 1) -- skip '{'
148
+ if s:sub(i, i) == '}' then return t, i + 1 end
149
+ while true do
150
+ i = skip_ws(s, i)
151
+ local k; k, i = decode_string(s, i)
152
+ i = skip_ws(s, i)
153
+ if s:sub(i, i) ~= ':' then error("json: expected ':' at " .. i) end
154
+ i = skip_ws(s, i + 1)
155
+ local v; v, i = decode_value(s, i)
156
+ t[k] = v
157
+ i = skip_ws(s, i)
158
+ local c = s:sub(i, i)
159
+ if c == '}' then return t, i + 1
160
+ elseif c == ',' then i = i + 1
161
+ else error("json: expected ',' or '}' at " .. i) end
162
+ end
163
+ end
164
+
165
+ local function decode_array(s, i)
166
+ local t = {}
167
+ i = skip_ws(s, i + 1) -- skip '['
168
+ if s:sub(i, i) == ']' then return t, i + 1 end
169
+ while true do
170
+ i = skip_ws(s, i)
171
+ local v; v, i = decode_value(s, i)
172
+ t[#t + 1] = v
173
+ i = skip_ws(s, i)
174
+ local c = s:sub(i, i)
175
+ if c == ']' then return t, i + 1
176
+ elseif c == ',' then i = i + 1
177
+ else error("json: expected ',' or ']' at " .. i) end
178
+ end
179
+ end
180
+
181
+ decode_value = function(s, i)
182
+ i = skip_ws(s, i)
183
+ if i > #s then error("json: unexpected end of input") end
184
+ local c = s:sub(i, i)
185
+ if c == '"' then return decode_string(s, i)
186
+ elseif c == '{' then return decode_object(s, i)
187
+ elseif c == '[' then return decode_array(s, i)
188
+ elseif c == 't' then return true, i + 4
189
+ elseif c == 'f' then return false, i + 5
190
+ elseif c == 'n' then return nil, i + 4
191
+ elseif c == '-' or c:match('%d') then return decode_number(s, i)
192
+ else error("json: unexpected character '" .. c .. "' at " .. i) end
193
+ end
194
+
195
+ function json.decode(s)
196
+ local v, _ = decode_value(s, 1)
197
+ return v
198
+ end
199
+
200
+ return json
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "mcp-mgba",
3
+ "version": "0.1.0",
4
+ "description": "MCP server that bridges Claude to the mGBA Game Boy Advance emulator via Lua scripting",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "mcp-mgba": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist/",
12
+ "lua/",
13
+ "docs/",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc && node -e \"import('node:fs').then(fs => fs.chmodSync('dist/index.js', 0o755))\"",
19
+ "dev": "tsc --watch",
20
+ "start": "node dist/index.js",
21
+ "prepare": "npm run build",
22
+ "inspector": "npx @modelcontextprotocol/inspector node dist/index.js"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/dmang-dev/mcp-mgba.git"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/dmang-dev/mcp-mgba/issues"
33
+ },
34
+ "homepage": "https://github.com/dmang-dev/mcp-mgba#readme",
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.12.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.0.0",
40
+ "typescript": "^5.5.0"
41
+ },
42
+ "keywords": [
43
+ "mcp",
44
+ "model-context-protocol",
45
+ "mgba",
46
+ "gba",
47
+ "game-boy-advance",
48
+ "emulator",
49
+ "lua"
50
+ ]
51
+ }