pendulum-mcp-dispatcher 1.0.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.
Files changed (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -0
  3. package/index.js +279 -0
  4. package/package.json +38 -0
  5. package/tools.js +275 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 IAFEnvoy
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
13
+ all 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
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # Pendulum MCP Dispatcher
2
+
3
+ Pendulum MCP dispatcher layer — a transparent proxy between AI clients and the Minecraft MCP server.
4
+
5
+ ## What is Pendulum MCP?
6
+
7
+ Pendulum is a Minecraft mod that exposes in-game actions as tools via an MCP (Minecraft Code Protocol) server. It allows AI agents to interact with the Minecraft world by calling these tools.
8
+
9
+ Currently, Pendulum support both Data Mode and Visual Mode tools. Data Mode provides JavaScript APIs for direct control (like `Playwright.js`), while Visual Mode allows agents to interact with the game through screenshots and simulated input.
10
+
11
+ For more details, see the [Pendulum GitHub repository](https://github.com/IAFEnvoy/Pendulum), you can also get compiled jars from [CurseForge](https://www.curseforge.com/minecraft/mc-mods/pendulum) and [Modrinth](https://modrinth.com/mod/pendulum). Also you can refer to [Documentation](https://docs.iafenvoy.com/docs/mod/pendulum) for the complete tool list and usage instructions.
12
+
13
+ ## Why a Dispatcher Layer
14
+
15
+ Pendulum's MCP server runs inside the Minecraft game (TCP port 25566). When the game is not running, AI clients (such as VS Code Copilot, Claude Desktop) cannot discover the tool list, causing **context errors**.
16
+
17
+ This dispatcher layer solves the problem:
18
+
19
+ - **Static tool descriptions**: Built-in complete definitions for 22 tools, so AI clients can see all tools even when the game is not running
20
+ - **Health check**: Verifies backend connectivity before every call; returns a clear error message when unreachable
21
+ - **Transparent forwarding**: Once the backend is ready, all requests are forwarded as-is with zero performance loss
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install -g pendulum-mcp-dispatcher
27
+ ```
28
+
29
+ Or use `npx` to run it on-the-fly without installing:
30
+
31
+ ```bash
32
+ npx pendulum-mcp-dispatcher
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ```bash
38
+ # Default connection to localhost:25566
39
+ pendulum-mcp-dispatcher
40
+
41
+ # Or via npx
42
+ npx pendulum-mcp-dispatcher
43
+
44
+ # Custom backend address
45
+ PENDULUM_HOST=192.168.1.100 PENDULUM_PORT=25566 pendulum-mcp-dispatcher
46
+ ```
47
+
48
+ ## Configuring AI Clients
49
+
50
+ ### VS Code Copilot
51
+
52
+ In `.vscode/mcp.json`:
53
+
54
+ ```json
55
+ {
56
+ "servers": {
57
+ "pendulum": {
58
+ "type": "stdio",
59
+ "command": "npx",
60
+ "args": ["-y", "pendulum-mcp-dispatcher"],
61
+ "env": {
62
+ "PENDULUM_HOST": "localhost",
63
+ "PENDULUM_PORT": "25566"
64
+ }
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ ### Claude Desktop
71
+
72
+ In `claude_desktop_config.json`:
73
+
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "pendulum": {
78
+ "command": "npx",
79
+ "args": ["-y", "pendulum-mcp-dispatcher"],
80
+ "env": {
81
+ "PENDULUM_HOST": "localhost",
82
+ "PENDULUM_PORT": "25566"
83
+ }
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ ## Workflow
90
+
91
+ ```
92
+ AI Client (Copilot/Claude)
93
+
94
+ │ stdio (MCP protocol)
95
+
96
+ ┌──────────────────────────┐
97
+ │ Pendulum Dispatcher │ ← You are here
98
+ │ - Static tool list │
99
+ │ - Health check │
100
+ │ - Request forwarding │
101
+ └────────┬─────────────────┘
102
+
103
+ │ TCP (JSON-RPC 2.0)
104
+
105
+ ┌──────────────────────────┐
106
+ │ Minecraft + Pendulum │
107
+ │ (localhost:25566) │
108
+ └──────────────────────────┘
109
+ ```
110
+
111
+ ## Error Handling
112
+
113
+ When Minecraft is not running, the AI client will receive:
114
+
115
+ ```
116
+ Pendulum MCP server is not reachable (127.0.0.1:25566).
117
+
118
+ Reason: Connection timed out — Minecraft may not be running
119
+
120
+ Please ensure:
121
+ 1. Minecraft is running
122
+ 2. The Pendulum mod is installed
123
+ 3. MCP server is started: /pendulum mcp start
124
+
125
+ Then retry your request.
126
+ ```
127
+
128
+ ## Notes
129
+
130
+ - **No automatic game launch**: You need to manually start Minecraft and run `/pendulum mcp start`, you can also make it run on game launch by configuring.
131
+ - **Independent connection per request**: Uses short-lived connections to avoid state issues with persistent TCP connections
132
+ - **120s timeout**: The `script/eval` timeout matches the backend
package/index.js ADDED
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Pendulum MCP Dispatcher
5
+ *
6
+ * A transparent forwarding layer between AI clients and the Pendulum MCP TCP server.
7
+ *
8
+ * Key features:
9
+ * - Exposes an MCP stdio server to AI clients (VS Code Copilot, Claude Desktop, etc.)
10
+ * - Forwards all requests to the Pendulum MCP TCP server (localhost:25566 by default)
11
+ * - Health-checks the backend; returns meaningful errors when Minecraft is not running
12
+ * - Provides static tool definitions so AI clients always see the full tool list
13
+ * - Does NOT auto-start Minecraft — the user must start the game + MCP server manually
14
+ */
15
+
16
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
17
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18
+ import {
19
+ CallToolRequestSchema,
20
+ ListToolsRequestSchema,
21
+ } from "@modelcontextprotocol/sdk/types.js";
22
+ import net from "net";
23
+ import { TOOL_DEFINITIONS } from "./tools.js";
24
+
25
+ // ═══════════════════════════════════════════════
26
+ // Configuration
27
+ // ═══════════════════════════════════════════════
28
+
29
+ const BACKEND_HOST = process.env.PENDULUM_HOST || "127.0.0.1";
30
+ const BACKEND_PORT = parseInt(process.env.PENDULUM_PORT || "25566", 10);
31
+ const HEALTH_TIMEOUT_MS = 3000; // How long to wait for backend health check
32
+ const REQUEST_TIMEOUT_MS = 130000; // 120s eval timeout + 10s buffer
33
+
34
+ // ═══════════════════════════════════════════════
35
+ // Backend health check
36
+ // ═══════════════════════════════════════════════
37
+
38
+ /**
39
+ * Test if the Pendulum MCP TCP server is reachable.
40
+ * We send a minimal JSON-RPC request (tools/list) to verify full connectivity.
41
+ */
42
+ function checkBackendHealth() {
43
+ return new Promise((resolve) => {
44
+ const socket = new net.Socket();
45
+ let buffer = "";
46
+ let resolved = false;
47
+
48
+ const finish = (ok, reason) => {
49
+ if (resolved) return;
50
+ resolved = true;
51
+ socket.destroy();
52
+ resolve({ ok, reason });
53
+ };
54
+
55
+ socket.setTimeout(HEALTH_TIMEOUT_MS);
56
+ socket.connect(BACKEND_PORT, BACKEND_HOST, () => {
57
+ // Send a minimal tools/list request to verify the full JSON-RPC pipeline
58
+ const req = JSON.stringify({
59
+ jsonrpc: "2.0",
60
+ id: "health",
61
+ method: "tools/list",
62
+ params: {},
63
+ });
64
+ socket.write(req + "\n");
65
+ });
66
+
67
+ socket.on("data", (data) => {
68
+ buffer += data.toString("utf-8");
69
+ try {
70
+ JSON.parse(buffer.trim());
71
+ finish(true, null);
72
+ } catch (_) {
73
+ // Partial response, wait for more data
74
+ }
75
+ });
76
+
77
+ socket.on("timeout", () => finish(false, "Connection timed out — Minecraft may not be running"));
78
+ socket.on("error", (err) => finish(false, err.message));
79
+ socket.on("close", () => finish(false, "Connection closed by backend"));
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Execute a raw JSON-RPC call against the Pendulum MCP TCP server.
85
+ * Opens a fresh connection for each request (simple, thread-safe).
86
+ */
87
+ function callBackend(method, params) {
88
+ return new Promise((resolve, reject) => {
89
+ const socket = new net.Socket();
90
+ let buffer = "";
91
+ let resolved = false;
92
+
93
+ const id = "disp-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
94
+ const request = JSON.stringify({
95
+ jsonrpc: "2.0",
96
+ id,
97
+ method,
98
+ params: params || {},
99
+ });
100
+
101
+ socket.setTimeout(REQUEST_TIMEOUT_MS);
102
+
103
+ socket.connect(BACKEND_PORT, BACKEND_HOST, () => {
104
+ socket.write(request + "\n");
105
+ });
106
+
107
+ socket.on("data", (data) => {
108
+ buffer += data.toString("utf-8");
109
+ try {
110
+ // Pendulum MCP TCP sends one JSON object per line
111
+ const lines = buffer.split("\n");
112
+ for (const line of lines) {
113
+ if (!line.trim()) continue;
114
+ const msg = JSON.parse(line);
115
+ if (msg.id === id || msg.method === "notifications/message") {
116
+ if (msg.result !== undefined) {
117
+ resolved = true;
118
+ socket.destroy();
119
+ resolve(msg.result);
120
+ return;
121
+ }
122
+ if (msg.error) {
123
+ resolved = true;
124
+ socket.destroy();
125
+ reject(new Error(msg.error.message || JSON.stringify(msg.error)));
126
+ return;
127
+ }
128
+ }
129
+ }
130
+ } catch (_) {
131
+ // Partial JSON, keep buffering
132
+ }
133
+ });
134
+
135
+ socket.on("error", (err) => {
136
+ if (resolved) return;
137
+ resolved = true;
138
+ reject(err);
139
+ });
140
+
141
+ socket.on("timeout", () => {
142
+ if (resolved) return;
143
+ resolved = true;
144
+ socket.destroy();
145
+ reject(new Error("Request timed out (120s+) — the script may be too long. Use script/evalAsync for long scripts."));
146
+ });
147
+
148
+ socket.on("close", () => {
149
+ if (resolved) return;
150
+ resolved = true;
151
+ reject(new Error("Backend closed the connection unexpectedly — may have crashed"));
152
+ });
153
+ });
154
+ }
155
+
156
+ // ═══════════════════════════════════════════════
157
+ // MCP Server (stdio transport → AI client)
158
+ // ═══════════════════════════════════════════════
159
+
160
+ async function main() {
161
+ const server = new Server(
162
+ {
163
+ name: "pendulum-dispatcher",
164
+ version: "1.0.0",
165
+ },
166
+ {
167
+ capabilities: {
168
+ tools: {},
169
+ },
170
+ }
171
+ );
172
+
173
+ // ── tools/list: always return static definitions ──
174
+ // This ensures the AI client always sees the full tool list even when
175
+ // Minecraft is not running, preventing context errors.
176
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
177
+ let health = "unknown";
178
+ try {
179
+ const result = await checkBackendHealth();
180
+ health = result.ok ? "connected" : result.reason;
181
+ } catch (_) {
182
+ health = "check failed";
183
+ }
184
+
185
+ // Append health status to the first tool's description as a hint
186
+ const tools = TOOL_DEFINITIONS.map((t, i) => {
187
+ if (i === 0 && health !== "connected") {
188
+ return {
189
+ ...t,
190
+ description: `[Backend: ${health}] ${t.description}`,
191
+ };
192
+ }
193
+ return t;
194
+ });
195
+
196
+ return { tools };
197
+ });
198
+
199
+ // ── tools/call: forward to backend ──
200
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
201
+ const toolName = request.params.name;
202
+ const args = request.params.arguments || {};
203
+
204
+ // 1) Health check
205
+ const health = await checkBackendHealth();
206
+ if (!health.ok) {
207
+ return {
208
+ content: [
209
+ {
210
+ type: "text",
211
+ text: `Pendulum MCP server is not reachable (${BACKEND_HOST}:${BACKEND_PORT}).\n\n` +
212
+ `Reason: ${health.reason}\n\n` +
213
+ `Please ensure:\n` +
214
+ `1. Minecraft is running\n` +
215
+ `2. The Pendulum mod is installed\n` +
216
+ `3. MCP server is started: /pendulum mcp start\n\n` +
217
+ `Then retry your request.`,
218
+ },
219
+ ],
220
+ isError: true,
221
+ };
222
+ }
223
+
224
+ // 2) Forward the request to the real Pendulum MCP server
225
+ try {
226
+ const result = await callBackend("tools/call", {
227
+ name: toolName,
228
+ arguments: args,
229
+ });
230
+
231
+ // Unwrap the result — Pendulum returns { content: [...] }
232
+ if (result && result.content) {
233
+ return { content: result.content };
234
+ }
235
+
236
+ // Some responses may be a plain string
237
+ if (typeof result === "string") {
238
+ return {
239
+ content: [{ type: "text", text: result }],
240
+ };
241
+ }
242
+
243
+ return {
244
+ content: [{ type: "text", text: JSON.stringify(result) }],
245
+ };
246
+ } catch (err) {
247
+ return {
248
+ content: [
249
+ {
250
+ type: "text",
251
+ text: `Pendulum MCP error: ${err.message}`,
252
+ },
253
+ ],
254
+ isError: true,
255
+ };
256
+ }
257
+ });
258
+
259
+ // ── Start the server ──
260
+ const transport = new StdioServerTransport();
261
+ await server.connect(transport);
262
+
263
+ console.error("[pendulum-dispatcher] Listening on stdio");
264
+ console.error(`[pendulum-dispatcher] Backend: ${BACKEND_HOST}:${BACKEND_PORT}`);
265
+
266
+ // Initial health check
267
+ const health = await checkBackendHealth();
268
+ if (health.ok) {
269
+ console.error("[pendulum-dispatcher] ✓ Backend is reachable");
270
+ } else {
271
+ console.error(`[pendulum-dispatcher] ⚠ Backend not reachable: ${health.reason}`);
272
+ console.error("[pendulum-dispatcher] Start Minecraft + /pendulum mcp start, then retry");
273
+ }
274
+ }
275
+
276
+ main().catch((err) => {
277
+ console.error("[pendulum-dispatcher] Fatal error:", err.message);
278
+ process.exit(1);
279
+ });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "pendulum-mcp-dispatcher",
3
+ "version": "1.0.0",
4
+ "description": "MCP forwarding layer for Pendulum — proxies requests to Minecraft MCP server",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "files": [
8
+ "index.js",
9
+ "tools.js"
10
+ ],
11
+ "scripts": {
12
+ "start": "node index.js"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "pendulum",
17
+ "minecraft",
18
+ "model-context-protocol",
19
+ "dispatcher",
20
+ "proxy"
21
+ ],
22
+ "author": "IAFEnvoy",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/IAFEnvoy/pendulum-mcp-dispatcher.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/IAFEnvoy/pendulum-mcp-dispatcher/issues"
30
+ },
31
+ "homepage": "https://github.com/IAFEnvoy/pendulum-mcp-dispatcher#readme",
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.0.0"
37
+ }
38
+ }
package/tools.js ADDED
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Pendulum MCP Tool Definitions
3
+ *
4
+ * Copied from the Pendulum mod's MCP server to ensure AI clients always
5
+ * see the full tool list even when the backend is unavailable.
6
+ *
7
+ * Structure: { name, description, inputSchema: { type, properties, required } }
8
+ */
9
+
10
+ export const TOOL_DEFINITIONS = [
11
+ // ═══════════════════════════════════════════
12
+ // Script Engine (script/*)
13
+ // ═══════════════════════════════════════════
14
+ {
15
+ name: "script/eval",
16
+ description: "Execute JavaScript code in Minecraft. Key globals: mc/minecraft/game. API: mc.player.* (movement, rotation, interaction, state), mc.world.* (blocks, entities, environment), mc.inv.* (inventory, container), mc.gui.* (screen click, type, enumerate widgets). All functions are synchronous. BARITONE: if installed, prefer br.* (br.goto, br.mine, br.follow, br.stop, br.isActive, br.command).",
17
+ inputSchema: {
18
+ type: "object",
19
+ properties: {
20
+ code: { type: "string", description: "JS code. E.g.: mc.player.forward(20); for(let b of mc.world.findBlocks('diamond_ore',16)) mc.player.breakBlockAt(b.x,b.y,b.z); JSON.stringify(mc.inv.getAllItems()); mc.world.rayTrace(5). If baritone: br.goto(100,64,200); br.mine('diamond_ore',64);" }
21
+ },
22
+ required: ["code"]
23
+ }
24
+ },
25
+ {
26
+ name: "script/evalAsync",
27
+ description: "Execute JavaScript asynchronously — returns immediately. Use 'script/status' to check completion. For scripts longer than 120s.",
28
+ inputSchema: {
29
+ type: "object",
30
+ properties: {
31
+ code: { type: "string", description: "JS code to execute asynchronously." }
32
+ },
33
+ required: ["code"]
34
+ }
35
+ },
36
+ {
37
+ name: "script/status",
38
+ description: "Check if a script is currently running.",
39
+ inputSchema: {
40
+ type: "object",
41
+ properties: {
42
+ _: { type: "string", description: "No parameters required" }
43
+ }
44
+ }
45
+ },
46
+ {
47
+ name: "script/abort",
48
+ description: "Abort the currently running script.",
49
+ inputSchema: {
50
+ type: "object",
51
+ properties: {
52
+ _: { type: "string", description: "No parameters required" }
53
+ }
54
+ }
55
+ },
56
+
57
+ // ═══════════════════════════════════════════
58
+ // Pendulum Core
59
+ // ═══════════════════════════════════════════
60
+ {
61
+ name: "health",
62
+ description: "Health check: reports screenshot capability, keyboard injection, Baritone availability, and script state.",
63
+ inputSchema: {
64
+ type: "object",
65
+ properties: {
66
+ _: { type: "string", description: "No parameters required" }
67
+ }
68
+ }
69
+ },
70
+
71
+ // ═══════════════════════════════════════════
72
+ // GUI (gui/*)
73
+ // ═══════════════════════════════════════════
74
+ {
75
+ name: "gui/screenshot",
76
+ description: "Capture a screenshot with coordinate grid overlay. Returns base64 PNG. Use optional 'path' to also save to disk.",
77
+ inputSchema: {
78
+ type: "object",
79
+ properties: {
80
+ path: { type: "string", description: "File path to save. Omit for base64 return only." }
81
+ }
82
+ }
83
+ },
84
+ {
85
+ name: "gui/enumerateWidgets",
86
+ description: "Recursively enumerate ALL GUI widgets including nested children. Returns [{type, text?, x, y, width, height, active?, focused?, children?}, ...].",
87
+ inputSchema: {
88
+ type: "object",
89
+ properties: {
90
+ _: { type: "string", description: "No parameters required" }
91
+ }
92
+ }
93
+ },
94
+ {
95
+ name: "gui/guiElements",
96
+ description: "Get flat list of non-slot GUI elements (buttons, labels). Returns [{type, x, y, width, height, text?}].",
97
+ inputSchema: {
98
+ type: "object",
99
+ properties: {
100
+ _: { type: "string", description: "No parameters required" }
101
+ }
102
+ }
103
+ },
104
+ {
105
+ name: "gui/clickButton",
106
+ description: "Find a button/widget by text substring or type name and click its center. Searches recursively. Returns widget info.",
107
+ inputSchema: {
108
+ type: "object",
109
+ properties: {
110
+ target: { type: "string", description: "Text to match (substring, case-insensitive) or widget type name." }
111
+ },
112
+ required: ["target"]
113
+ }
114
+ },
115
+
116
+ // ═══════════════════════════════════════════
117
+ // Simulate Input (simulate/*)
118
+ // ═══════════════════════════════════════════
119
+ {
120
+ name: "simulate/click",
121
+ description: "Click at screen coordinates. Use after 'gui/screenshot' to target UI elements. Screenshot includes a coordinate grid.",
122
+ inputSchema: {
123
+ type: "object",
124
+ properties: {
125
+ x: { type: "integer", description: "X coordinate in screen pixels." },
126
+ y: { type: "integer", description: "Y coordinate in screen pixels." },
127
+ button: { type: "string", description: "Mouse button: 'left' (default), 'right', or 'middle'." }
128
+ },
129
+ required: ["x", "y"]
130
+ }
131
+ },
132
+ {
133
+ name: "simulate/pressKey",
134
+ description: "Press a keyboard key. Supports: W, Enter, ESC, SPACE, F3, A, etc. Can hold for N seconds.",
135
+ inputSchema: {
136
+ type: "object",
137
+ properties: {
138
+ key: { type: "string", description: "Key name, e.g. 'W', 'Enter', 'ESC', 'SPACE', 'F3'." },
139
+ holdSeconds: { type: "number", description: "Duration to hold in seconds. Default 0 (press and release)." }
140
+ },
141
+ required: ["key"]
142
+ }
143
+ },
144
+ {
145
+ name: "simulate/typeText",
146
+ description: "Type text into the focused text field character by character.",
147
+ inputSchema: {
148
+ type: "object",
149
+ properties: {
150
+ text: { type: "string", description: "Text to type." },
151
+ pressEnter: { type: "boolean", description: "Press Enter after typing. Default false." }
152
+ },
153
+ required: ["text"]
154
+ }
155
+ },
156
+ {
157
+ name: "simulate/pasteText",
158
+ description: "Type text quickly (same as typeText, for large blocks).",
159
+ inputSchema: {
160
+ type: "object",
161
+ properties: {
162
+ text: { type: "string", description: "Text to paste." },
163
+ pressEnter: { type: "boolean", description: "Press Enter after. Default false." }
164
+ },
165
+ required: ["text"]
166
+ }
167
+ },
168
+ {
169
+ name: "simulate/scroll",
170
+ description: "Scroll mouse wheel. Positive = up, negative = down.",
171
+ inputSchema: {
172
+ type: "object",
173
+ properties: {
174
+ clicks: { type: "integer", description: "Number of scroll clicks." }
175
+ },
176
+ required: ["clicks"]
177
+ }
178
+ },
179
+ {
180
+ name: "simulate/hotkey",
181
+ description: "Press a key combination. E.g. 'ctrl,s' or 'shift,f3'.",
182
+ inputSchema: {
183
+ type: "object",
184
+ properties: {
185
+ keys: { type: "string", description: "Comma-separated key names." }
186
+ },
187
+ required: ["keys"]
188
+ }
189
+ },
190
+ {
191
+ name: "simulate/mouseDrag",
192
+ description: "Drag mouse from one point to another.",
193
+ inputSchema: {
194
+ type: "object",
195
+ properties: {
196
+ xStart: { type: "integer", description: "Start X." },
197
+ yStart: { type: "integer", description: "Start Y." },
198
+ xEnd: { type: "integer", description: "End X." },
199
+ yEnd: { type: "integer", description: "End Y." },
200
+ button: { type: "string", description: "Mouse button: 'left' (default), 'right', or 'middle'." }
201
+ },
202
+ required: ["xStart", "yStart", "xEnd", "yEnd"]
203
+ }
204
+ },
205
+ {
206
+ name: "simulate/callScreenMethod",
207
+ description: "DANGEROUS — Call an arbitrary no-arg method on the current GUI screen via reflection. All exceptions caught.",
208
+ inputSchema: {
209
+ type: "object",
210
+ properties: {
211
+ method: { type: "string", description: "Method name to call on the screen object." }
212
+ },
213
+ required: ["method"]
214
+ }
215
+ },
216
+ {
217
+ name: "simulate/selectListItem",
218
+ description: "Select an item from a dropdown/list widget by text substring (case-insensitive).",
219
+ inputSchema: {
220
+ type: "object",
221
+ properties: {
222
+ text: { type: "string", description: "Text substring to match." }
223
+ },
224
+ required: ["text"]
225
+ }
226
+ },
227
+
228
+ // ═══════════════════════════════════════════
229
+ // Utility
230
+ // ═══════════════════════════════════════════
231
+ {
232
+ name: "wait",
233
+ description: "Wait for N seconds. Useful for sequencing actions.",
234
+ inputSchema: {
235
+ type: "object",
236
+ properties: {
237
+ seconds: { type: "number", description: "Seconds to wait. Default 1.0." }
238
+ }
239
+ }
240
+ },
241
+
242
+ // ═══════════════════════════════════════════
243
+ // Video (video/*)
244
+ // ═══════════════════════════════════════════
245
+ {
246
+ name: "video/start",
247
+ description: "EXPERIMENTAL — Start ~10fps video capture. Reads GPU every 6 frames — expensive. Prefer 'gui/screenshot' for single shots.",
248
+ inputSchema: {
249
+ type: "object",
250
+ properties: {
251
+ _: { type: "string", description: "No parameters required" }
252
+ }
253
+ }
254
+ },
255
+ {
256
+ name: "video/stop",
257
+ description: "Stop video frame capture.",
258
+ inputSchema: {
259
+ type: "object",
260
+ properties: {
261
+ _: { type: "string", description: "No parameters required" }
262
+ }
263
+ }
264
+ },
265
+ {
266
+ name: "video/frame",
267
+ description: "Get latest cached video frame as base64 PNG. Error if no recent frame (< 5s).",
268
+ inputSchema: {
269
+ type: "object",
270
+ properties: {
271
+ _: { type: "string", description: "No parameters required" }
272
+ }
273
+ }
274
+ }
275
+ ];