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.
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/index.js +279 -0
- package/package.json +38 -0
- 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
|
+
];
|