crosspad-mcp-server 4.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/README.md +187 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +33 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +360 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/architecture.d.ts +16 -0
- package/dist/tools/architecture.js +198 -0
- package/dist/tools/architecture.js.map +1 -0
- package/dist/tools/build-check.d.ts +23 -0
- package/dist/tools/build-check.js +162 -0
- package/dist/tools/build-check.js.map +1 -0
- package/dist/tools/build.d.ts +14 -0
- package/dist/tools/build.js +101 -0
- package/dist/tools/build.js.map +1 -0
- package/dist/tools/diff-core.d.ts +24 -0
- package/dist/tools/diff-core.js +88 -0
- package/dist/tools/diff-core.js.map +1 -0
- package/dist/tools/idf-build.d.ts +10 -0
- package/dist/tools/idf-build.js +155 -0
- package/dist/tools/idf-build.js.map +1 -0
- package/dist/tools/input.d.ts +36 -0
- package/dist/tools/input.js +61 -0
- package/dist/tools/input.js.map +1 -0
- package/dist/tools/log.d.ts +16 -0
- package/dist/tools/log.js +49 -0
- package/dist/tools/log.js.map +1 -0
- package/dist/tools/repos.d.ts +12 -0
- package/dist/tools/repos.js +63 -0
- package/dist/tools/repos.js.map +1 -0
- package/dist/tools/scaffold.d.ts +15 -0
- package/dist/tools/scaffold.js +192 -0
- package/dist/tools/scaffold.js.map +1 -0
- package/dist/tools/screenshot.d.ts +24 -0
- package/dist/tools/screenshot.js +80 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/settings.d.ts +25 -0
- package/dist/tools/settings.js +48 -0
- package/dist/tools/settings.js.map +1 -0
- package/dist/tools/stats.d.ts +18 -0
- package/dist/tools/stats.js +31 -0
- package/dist/tools/stats.js.map +1 -0
- package/dist/tools/symbols.d.ts +20 -0
- package/dist/tools/symbols.js +157 -0
- package/dist/tools/symbols.js.map +1 -0
- package/dist/tools/test.d.ts +24 -0
- package/dist/tools/test.js +227 -0
- package/dist/tools/test.js.map +1 -0
- package/dist/utils/exec.d.ts +58 -0
- package/dist/utils/exec.js +292 -0
- package/dist/utils/exec.js.map +1 -0
- package/dist/utils/git.d.ts +10 -0
- package/dist/utils/git.js +29 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/remote-client.d.ts +17 -0
- package/dist/utils/remote-client.js +94 -0
- package/dist/utils/remote-client.js.map +1 -0
- package/package.json +21 -0
- package/server.json +23 -0
- package/src/config.ts +45 -0
- package/src/index.ts +484 -0
- package/src/tools/architecture.ts +260 -0
- package/src/tools/build-check.ts +178 -0
- package/src/tools/build.ts +130 -0
- package/src/tools/diff-core.ts +130 -0
- package/src/tools/idf-build.ts +182 -0
- package/src/tools/input.ts +80 -0
- package/src/tools/log.ts +75 -0
- package/src/tools/repos.ts +75 -0
- package/src/tools/scaffold.ts +229 -0
- package/src/tools/screenshot.ts +100 -0
- package/src/tools/settings.ts +68 -0
- package/src/tools/stats.ts +38 -0
- package/src/tools/symbols.ts +185 -0
- package/src/tools/test.ts +264 -0
- package/src/utils/exec.ts +376 -0
- package/src/utils/git.ts +45 -0
- package/src/utils/remote-client.ts +107 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TCP client for communicating with the CrossPad simulator's remote control server.
|
|
3
|
+
* Protocol: newline-delimited JSON over TCP on localhost:19840.
|
|
4
|
+
*/
|
|
5
|
+
import { Socket } from "net";
|
|
6
|
+
const REMOTE_PORT = 19840;
|
|
7
|
+
const REMOTE_HOST = "127.0.0.1";
|
|
8
|
+
const CONNECT_TIMEOUT = 3000;
|
|
9
|
+
const RESPONSE_TIMEOUT = 15000;
|
|
10
|
+
/**
|
|
11
|
+
* Send a JSON command to the running simulator and return the response.
|
|
12
|
+
* Opens a fresh TCP connection per call (simple, stateless).
|
|
13
|
+
*/
|
|
14
|
+
export function sendRemoteCommand(command) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const socket = new Socket();
|
|
17
|
+
let buffer = "";
|
|
18
|
+
let resolved = false;
|
|
19
|
+
const cleanup = () => {
|
|
20
|
+
if (!resolved) {
|
|
21
|
+
resolved = true;
|
|
22
|
+
socket.destroy();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
// Connect timeout
|
|
26
|
+
socket.setTimeout(CONNECT_TIMEOUT);
|
|
27
|
+
socket.on("connect", () => {
|
|
28
|
+
// Extend timeout for response
|
|
29
|
+
socket.setTimeout(RESPONSE_TIMEOUT);
|
|
30
|
+
// Send command as newline-delimited JSON
|
|
31
|
+
const msg = JSON.stringify(command) + "\n";
|
|
32
|
+
socket.write(msg);
|
|
33
|
+
});
|
|
34
|
+
socket.on("data", (data) => {
|
|
35
|
+
buffer += data.toString();
|
|
36
|
+
// Look for newline-delimited response
|
|
37
|
+
const nlIdx = buffer.indexOf("\n");
|
|
38
|
+
if (nlIdx >= 0) {
|
|
39
|
+
const line = buffer.slice(0, nlIdx);
|
|
40
|
+
resolved = true;
|
|
41
|
+
socket.destroy();
|
|
42
|
+
try {
|
|
43
|
+
resolve(JSON.parse(line));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
resolve({ ok: false, error: "invalid JSON response", raw: line });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
socket.on("timeout", () => {
|
|
51
|
+
cleanup();
|
|
52
|
+
reject(new Error("Connection/response timeout — is the simulator running?"));
|
|
53
|
+
});
|
|
54
|
+
socket.on("error", (err) => {
|
|
55
|
+
cleanup();
|
|
56
|
+
if (err.code === "ECONNREFUSED") {
|
|
57
|
+
reject(new Error("Connection refused — simulator is not running or remote control is disabled. Start with crosspad_run first."));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
reject(new Error(`TCP error: ${err.message}`));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
socket.on("close", () => {
|
|
64
|
+
if (!resolved) {
|
|
65
|
+
resolved = true;
|
|
66
|
+
if (buffer.length > 0) {
|
|
67
|
+
try {
|
|
68
|
+
resolve(JSON.parse(buffer));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
resolve({ ok: false, error: "incomplete response", raw: buffer });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
reject(new Error("Connection closed without response"));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
socket.connect(REMOTE_PORT, REMOTE_HOST);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Check if the simulator's remote control server is reachable.
|
|
84
|
+
*/
|
|
85
|
+
export async function isSimulatorRunning() {
|
|
86
|
+
try {
|
|
87
|
+
const resp = await sendRemoteCommand({ cmd: "ping" });
|
|
88
|
+
return resp.ok === true;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=remote-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-client.js","sourceRoot":"","sources":["../../src/utils/remote-client.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "crosspad-mcp-server",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "MCP development server for CrossPad",
|
|
5
|
+
"mcpName": "io.github.crosspad/crosspad-mcp",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"prepare": "tsc",
|
|
11
|
+
"dev": "tsc --watch"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
15
|
+
"zod": "^4.3.6"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.0.0",
|
|
19
|
+
"typescript": "^5.7.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
|
|
3
|
+
"name": "io.github.crosspad/crosspad-mcp",
|
|
4
|
+
"description": "Development workflow server for CrossPad — build, test, run, screenshot, and navigate code",
|
|
5
|
+
"version": "4.0.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/CrossPad/crosspad-mcp",
|
|
8
|
+
"source": "github",
|
|
9
|
+
"id": "CrossPad/crosspad-mcp"
|
|
10
|
+
},
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registry_type": "npm",
|
|
14
|
+
"registry_base_url": "https://registry.npmjs.org",
|
|
15
|
+
"identifier": "crosspad-mcp-server",
|
|
16
|
+
"version": "4.0.0",
|
|
17
|
+
"runtime_hint": "npx",
|
|
18
|
+
"transport": {
|
|
19
|
+
"type": "stdio"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import os from "os";
|
|
3
|
+
|
|
4
|
+
export const IS_WINDOWS = process.platform === "win32";
|
|
5
|
+
export const IS_MAC = process.platform === "darwin";
|
|
6
|
+
|
|
7
|
+
// Base directory for all CrossPad repos (override with CROSSPAD_GIT_DIR)
|
|
8
|
+
const defaultGitDir = IS_WINDOWS
|
|
9
|
+
? "C:/Users/Mateusz/GIT"
|
|
10
|
+
: path.join(os.homedir(), "GIT");
|
|
11
|
+
const GIT_DIR = process.env.CROSSPAD_GIT_DIR || defaultGitDir;
|
|
12
|
+
|
|
13
|
+
export const CROSSPAD_PC_ROOT =
|
|
14
|
+
process.env.CROSSPAD_PC_ROOT || path.join(GIT_DIR, "crosspad-pc");
|
|
15
|
+
|
|
16
|
+
export const REPOS: Record<string, string> = {
|
|
17
|
+
"crosspad-core": path.join(GIT_DIR, "crosspad-core"),
|
|
18
|
+
"crosspad-gui": path.join(GIT_DIR, "crosspad-gui"),
|
|
19
|
+
"crosspad-pc": CROSSPAD_PC_ROOT,
|
|
20
|
+
"ESP32-S3": path.join(GIT_DIR, "ESP32-S3"),
|
|
21
|
+
"2playerCrosspad": path.join(GIT_DIR, "P4_TEST", "2playerCrosspad"),
|
|
22
|
+
"crosspad-idf": path.join(GIT_DIR, "crosspad-idf"),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// MSVC — only used on Windows
|
|
26
|
+
export const VCVARSALL =
|
|
27
|
+
process.env.VCVARSALL ||
|
|
28
|
+
"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Auxiliary\\Build\\vcvarsall.bat";
|
|
29
|
+
|
|
30
|
+
// vcpkg — platform-aware defaults
|
|
31
|
+
const defaultVcpkgRoot = IS_WINDOWS ? "C:/vcpkg" : path.join(os.homedir(), "vcpkg");
|
|
32
|
+
const vcpkgRoot = process.env.VCPKG_ROOT || defaultVcpkgRoot;
|
|
33
|
+
export const VCPKG_TOOLCHAIN = path.join(vcpkgRoot, "scripts", "buildsystems", "vcpkg.cmake");
|
|
34
|
+
|
|
35
|
+
export const BUILD_DIR = path.join(CROSSPAD_PC_ROOT, "build");
|
|
36
|
+
|
|
37
|
+
const EXE_EXT = IS_WINDOWS ? ".exe" : "";
|
|
38
|
+
export const BIN_EXE = path.join(CROSSPAD_PC_ROOT, "bin", `CrossPad${EXE_EXT}`);
|
|
39
|
+
|
|
40
|
+
// ESP-IDF
|
|
41
|
+
export const CROSSPAD_IDF_ROOT =
|
|
42
|
+
process.env.CROSSPAD_IDF_ROOT || path.join(GIT_DIR, "crosspad-idf");
|
|
43
|
+
|
|
44
|
+
const defaultIdfPath = path.join(os.homedir(), "esp", "v5.5", "esp-idf");
|
|
45
|
+
export const IDF_PATH = process.env.IDF_PATH || defaultIdfPath;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { crosspadBuild, crosspadRun } from "./tools/build.js";
|
|
7
|
+
import { crosspadReposStatus } from "./tools/repos.js";
|
|
8
|
+
import { crosspadScaffoldApp } from "./tools/scaffold.js";
|
|
9
|
+
import { crosspadInterfaces, crosspadApps } from "./tools/architecture.js";
|
|
10
|
+
import { crosspadBuildCheck } from "./tools/build-check.js";
|
|
11
|
+
import { crosspadSearchSymbols } from "./tools/symbols.js";
|
|
12
|
+
import { crosspadDiffCore } from "./tools/diff-core.js";
|
|
13
|
+
import { crosspadLog } from "./tools/log.js";
|
|
14
|
+
import { crosspadTest, crosspadTestScaffold } from "./tools/test.js";
|
|
15
|
+
import { crosspadScreenshot } from "./tools/screenshot.js";
|
|
16
|
+
import { crosspadInput, InputAction } from "./tools/input.js";
|
|
17
|
+
import { crosspadSettingsGet, crosspadSettingsSet } from "./tools/settings.js";
|
|
18
|
+
import { crosspadStats } from "./tools/stats.js";
|
|
19
|
+
import { crosspadIdfBuild } from "./tools/idf-build.js";
|
|
20
|
+
import type { OnLine } from "./utils/exec.js";
|
|
21
|
+
import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
|
|
22
|
+
|
|
23
|
+
const server = new McpServer(
|
|
24
|
+
{
|
|
25
|
+
name: "crosspad",
|
|
26
|
+
version: "4.0.0",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
capabilities: {
|
|
30
|
+
logging: {},
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create an OnLine callback that streams each line to the MCP client
|
|
37
|
+
* via logging notifications. Build output is "info", errors are "error".
|
|
38
|
+
*/
|
|
39
|
+
function makeStreamLogger(logger: string): OnLine {
|
|
40
|
+
return (stream, line) => {
|
|
41
|
+
if (!line.trim()) return; // skip empty lines
|
|
42
|
+
const level: LoggingLevel = stream === "stderr" ? "warning" : "info";
|
|
43
|
+
server.server.sendLoggingMessage({ level, logger, data: line }).catch(() => {});
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
48
|
+
// BUILD & RUN
|
|
49
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
50
|
+
|
|
51
|
+
server.tool(
|
|
52
|
+
"crosspad_build",
|
|
53
|
+
"Build crosspad-pc simulator (incremental, clean, or reconfigure)",
|
|
54
|
+
{
|
|
55
|
+
mode: z
|
|
56
|
+
.enum(["incremental", "clean", "reconfigure"])
|
|
57
|
+
.default("incremental")
|
|
58
|
+
.describe(
|
|
59
|
+
"incremental: just cmake --build. clean: delete build dir + full rebuild. reconfigure: cmake configure + build (use after adding new source files)"
|
|
60
|
+
),
|
|
61
|
+
},
|
|
62
|
+
async ({ mode }) => {
|
|
63
|
+
const onLine = makeStreamLogger("build");
|
|
64
|
+
const result = await crosspadBuild(mode, onLine);
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
server.tool(
|
|
72
|
+
"crosspad_run",
|
|
73
|
+
"Launch the crosspad-pc simulator (bin/CrossPad.exe). Returns immediately with PID.",
|
|
74
|
+
{},
|
|
75
|
+
async () => {
|
|
76
|
+
const result = crosspadRun();
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
server.tool(
|
|
84
|
+
"crosspad_build_check",
|
|
85
|
+
"Quick health check: is the build up to date? Detects stale exe, new source files needing reconfigure, submodule drift, dirty working trees. Use before build to know what mode to use.",
|
|
86
|
+
{},
|
|
87
|
+
async () => {
|
|
88
|
+
const result = crosspadBuildCheck();
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
server.tool(
|
|
96
|
+
"crosspad_log",
|
|
97
|
+
"Launch CrossPad.exe, capture stdout/stderr for a few seconds, then kill it. Great for checking init sequence, crash messages, or runtime errors without leaving the process running.",
|
|
98
|
+
{
|
|
99
|
+
timeout_seconds: z
|
|
100
|
+
.number()
|
|
101
|
+
.default(5)
|
|
102
|
+
.describe("How long to let the process run before killing it (default: 5)"),
|
|
103
|
+
max_lines: z
|
|
104
|
+
.number()
|
|
105
|
+
.default(200)
|
|
106
|
+
.describe("Max lines of output to return (default: 200)"),
|
|
107
|
+
},
|
|
108
|
+
async ({ timeout_seconds, max_lines }) => {
|
|
109
|
+
const onLine = makeStreamLogger("log");
|
|
110
|
+
const result = await crosspadLog(timeout_seconds, max_lines, onLine);
|
|
111
|
+
return {
|
|
112
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
118
|
+
// ESP-IDF BUILD
|
|
119
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
120
|
+
|
|
121
|
+
server.tool(
|
|
122
|
+
"crosspad_idf_build",
|
|
123
|
+
"Build the crosspad-idf ESP32-S3 firmware using idf.py. Use fullclean after adding new app directories or CMakeLists.txt changes.",
|
|
124
|
+
{
|
|
125
|
+
mode: z
|
|
126
|
+
.enum(["build", "fullclean", "clean"])
|
|
127
|
+
.default("build")
|
|
128
|
+
.describe(
|
|
129
|
+
"build: incremental idf.py build. fullclean: idf.py fullclean + build (required after new app dirs). clean: delete build/ + build."
|
|
130
|
+
),
|
|
131
|
+
},
|
|
132
|
+
async ({ mode }) => {
|
|
133
|
+
const onLine = makeStreamLogger("idf-build");
|
|
134
|
+
const result = await crosspadIdfBuild(mode, onLine);
|
|
135
|
+
return {
|
|
136
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
142
|
+
// TEST
|
|
143
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
144
|
+
|
|
145
|
+
server.tool(
|
|
146
|
+
"crosspad_test",
|
|
147
|
+
"Build and run the Catch2 test suite. If no tests/ dir exists, tells you to scaffold. Supports filtering by test name.",
|
|
148
|
+
{
|
|
149
|
+
filter: z
|
|
150
|
+
.string()
|
|
151
|
+
.default("")
|
|
152
|
+
.describe("Catch2 test name filter (e.g. '[core]' or 'PadManager')"),
|
|
153
|
+
list_only: z
|
|
154
|
+
.boolean()
|
|
155
|
+
.default(false)
|
|
156
|
+
.describe("Just list available tests without running them"),
|
|
157
|
+
},
|
|
158
|
+
async ({ filter, list_only }) => {
|
|
159
|
+
const onLine = makeStreamLogger("test");
|
|
160
|
+
const result = await crosspadTest(filter, list_only, onLine);
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
server.tool(
|
|
168
|
+
"crosspad_test_scaffold",
|
|
169
|
+
"Generate test infrastructure: tests/CMakeLists.txt (Catch2 v3), sample test file, and CMake patch instructions. Returns file contents — does NOT write to disk.",
|
|
170
|
+
{},
|
|
171
|
+
async () => {
|
|
172
|
+
const result = crosspadTestScaffold();
|
|
173
|
+
return {
|
|
174
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
180
|
+
// REPOS & SUBMODULES
|
|
181
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
182
|
+
|
|
183
|
+
server.tool(
|
|
184
|
+
"crosspad_repos_status",
|
|
185
|
+
"Show git status across all CrossPad repos (crosspad-core, crosspad-gui, crosspad-pc, ESP32-S3, 2playerCrosspad). Detects dev-mode vs submodule-mode and checks submodule pin sync.",
|
|
186
|
+
{},
|
|
187
|
+
async () => {
|
|
188
|
+
const result = crosspadReposStatus();
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
server.tool(
|
|
196
|
+
"crosspad_diff_core",
|
|
197
|
+
"Show what changed in crosspad-core and/or crosspad-gui relative to the pinned submodule commit. Shows commits ahead/behind, changed files, uncommitted changes. Essential for dev-mode workflows.",
|
|
198
|
+
{
|
|
199
|
+
submodule: z
|
|
200
|
+
.enum(["crosspad-core", "crosspad-gui", "both"])
|
|
201
|
+
.default("both")
|
|
202
|
+
.describe("Which submodule(s) to diff"),
|
|
203
|
+
},
|
|
204
|
+
async ({ submodule }) => {
|
|
205
|
+
const result = crosspadDiffCore(submodule);
|
|
206
|
+
return {
|
|
207
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
213
|
+
// CODE & ARCHITECTURE
|
|
214
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
215
|
+
|
|
216
|
+
server.tool(
|
|
217
|
+
"crosspad_search_symbols",
|
|
218
|
+
"Search for classes, functions, macros, enums across all CrossPad repos. Faster than manual grep — uses git grep under the hood.",
|
|
219
|
+
{
|
|
220
|
+
query: z.string().describe("Symbol name or substring to search for"),
|
|
221
|
+
kind: z
|
|
222
|
+
.enum(["class", "function", "macro", "enum", "typedef", "all"])
|
|
223
|
+
.default("all")
|
|
224
|
+
.describe("Filter by symbol kind"),
|
|
225
|
+
repos: z
|
|
226
|
+
.array(z.string())
|
|
227
|
+
.default(["all"])
|
|
228
|
+
.describe('Repo names to search: "crosspad-core", "crosspad-pc", "ESP32-S3", etc. or ["all"]'),
|
|
229
|
+
max_results: z
|
|
230
|
+
.number()
|
|
231
|
+
.default(50)
|
|
232
|
+
.describe("Max results to return"),
|
|
233
|
+
},
|
|
234
|
+
async ({ query, kind, repos, max_results }) => {
|
|
235
|
+
const result = crosspadSearchSymbols(query, kind, repos, max_results);
|
|
236
|
+
return {
|
|
237
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
server.tool(
|
|
243
|
+
"crosspad_scaffold_app",
|
|
244
|
+
"Generate boilerplate for a new CrossPad app (cpp, hpp, CMakeLists.txt, optional pad logic). Returns file contents for Claude to write — does NOT create files on disk.",
|
|
245
|
+
{
|
|
246
|
+
name: z
|
|
247
|
+
.string()
|
|
248
|
+
.describe("PascalCase app name, e.g. 'Metronome'"),
|
|
249
|
+
display_name: z
|
|
250
|
+
.string()
|
|
251
|
+
.optional()
|
|
252
|
+
.describe("Human-readable name (defaults to name)"),
|
|
253
|
+
has_pad_logic: z
|
|
254
|
+
.boolean()
|
|
255
|
+
.default(false)
|
|
256
|
+
.describe("Generate IPadLogicHandler stub"),
|
|
257
|
+
icon: z
|
|
258
|
+
.string()
|
|
259
|
+
.default("CrossPad_Logo_110w.png")
|
|
260
|
+
.describe("Icon filename"),
|
|
261
|
+
},
|
|
262
|
+
async ({ name, display_name, has_pad_logic, icon }) => {
|
|
263
|
+
const result = crosspadScaffoldApp({
|
|
264
|
+
name,
|
|
265
|
+
display_name,
|
|
266
|
+
has_pad_logic,
|
|
267
|
+
icon,
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
server.tool(
|
|
276
|
+
"crosspad_interfaces",
|
|
277
|
+
'Query crosspad-core interfaces and their implementations across all platforms. Use query="list" to list all interfaces, "implementations <InterfaceName>" to find implementations, or "capabilities" to show platform capability flags.',
|
|
278
|
+
{
|
|
279
|
+
query: z
|
|
280
|
+
.string()
|
|
281
|
+
.describe(
|
|
282
|
+
'"list", "implementations <InterfaceName>", or "capabilities"'
|
|
283
|
+
),
|
|
284
|
+
},
|
|
285
|
+
async ({ query }) => {
|
|
286
|
+
const result = crosspadInterfaces(query);
|
|
287
|
+
return {
|
|
288
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
server.tool(
|
|
294
|
+
"crosspad_apps",
|
|
295
|
+
"List all registered CrossPad apps (found via REGISTER_APP macro or _register_*_app functions).",
|
|
296
|
+
{
|
|
297
|
+
platform: z
|
|
298
|
+
.enum(["pc", "esp32", "2player", "all"])
|
|
299
|
+
.default("pc")
|
|
300
|
+
.describe("Which platform to scan"),
|
|
301
|
+
},
|
|
302
|
+
async ({ platform }) => {
|
|
303
|
+
const result = crosspadApps(platform);
|
|
304
|
+
return {
|
|
305
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
311
|
+
// SIMULATOR INTERACTION (requires running simulator with remote control)
|
|
312
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
313
|
+
|
|
314
|
+
server.tool(
|
|
315
|
+
"crosspad_screenshot",
|
|
316
|
+
"Capture a screenshot from the running CrossPad simulator. Saves PNG to screenshots/ dir by default. Requires the simulator to be running (use crosspad_run first).",
|
|
317
|
+
{
|
|
318
|
+
save_to_file: z
|
|
319
|
+
.boolean()
|
|
320
|
+
.default(true)
|
|
321
|
+
.describe("Save to disk (default: true). If false, returns base64 data inline."),
|
|
322
|
+
filename: z
|
|
323
|
+
.string()
|
|
324
|
+
.optional()
|
|
325
|
+
.describe("Custom filename (default: screenshot_<timestamp>.png)"),
|
|
326
|
+
region: z
|
|
327
|
+
.enum(["full", "lcd"])
|
|
328
|
+
.default("full")
|
|
329
|
+
.describe("full: entire simulator window (490x680). lcd: LCD screen only (320x240)."),
|
|
330
|
+
},
|
|
331
|
+
async ({ save_to_file, filename, region }) => {
|
|
332
|
+
const result = await crosspadScreenshot(save_to_file, filename, region);
|
|
333
|
+
return {
|
|
334
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
server.tool(
|
|
340
|
+
"crosspad_input",
|
|
341
|
+
`Send input events to the running CrossPad simulator. Requires simulator to be running.
|
|
342
|
+
|
|
343
|
+
Actions:
|
|
344
|
+
click {x, y} — mouse click at window coordinates (490x680 window)
|
|
345
|
+
pad_press {pad, vel} — press pad 0-15 with velocity 0-127
|
|
346
|
+
pad_release {pad} — release pad 0-15
|
|
347
|
+
encoder_rotate {delta} — rotate encoder (positive=CW, negative=CCW)
|
|
348
|
+
encoder_press — press encoder button
|
|
349
|
+
encoder_release — release encoder button
|
|
350
|
+
key {keycode} — SDL keycode (e.g. 13=Enter, 27=Escape)
|
|
351
|
+
|
|
352
|
+
LCD area is centered at roughly x=85..405, y=20..260 within the 490x680 window.`,
|
|
353
|
+
{
|
|
354
|
+
action: z
|
|
355
|
+
.enum(["click", "pad_press", "pad_release", "encoder_rotate", "encoder_press", "encoder_release", "key"])
|
|
356
|
+
.describe("Input action type"),
|
|
357
|
+
x: z.number().optional().describe("X coordinate for click"),
|
|
358
|
+
y: z.number().optional().describe("Y coordinate for click"),
|
|
359
|
+
pad: z.number().optional().describe("Pad index 0-15 for pad_press/pad_release"),
|
|
360
|
+
velocity: z.number().optional().describe("Velocity 0-127 for pad_press (default: 127)"),
|
|
361
|
+
delta: z.number().optional().describe("Rotation delta for encoder_rotate"),
|
|
362
|
+
keycode: z.number().optional().describe("SDL keycode for key action"),
|
|
363
|
+
},
|
|
364
|
+
async ({ action, x, y, pad, velocity, delta, keycode }) => {
|
|
365
|
+
let input: InputAction;
|
|
366
|
+
|
|
367
|
+
switch (action) {
|
|
368
|
+
case "click":
|
|
369
|
+
input = { action: "click", x: x ?? 0, y: y ?? 0 };
|
|
370
|
+
break;
|
|
371
|
+
case "pad_press":
|
|
372
|
+
input = { action: "pad_press", pad: pad ?? 0, velocity: velocity ?? 127 };
|
|
373
|
+
break;
|
|
374
|
+
case "pad_release":
|
|
375
|
+
input = { action: "pad_release", pad: pad ?? 0 };
|
|
376
|
+
break;
|
|
377
|
+
case "encoder_rotate":
|
|
378
|
+
input = { action: "encoder_rotate", delta: delta ?? 1 };
|
|
379
|
+
break;
|
|
380
|
+
case "encoder_press":
|
|
381
|
+
input = { action: "encoder_press" };
|
|
382
|
+
break;
|
|
383
|
+
case "encoder_release":
|
|
384
|
+
input = { action: "encoder_release" };
|
|
385
|
+
break;
|
|
386
|
+
case "key":
|
|
387
|
+
input = { action: "key", keycode: keycode ?? 0 };
|
|
388
|
+
break;
|
|
389
|
+
default:
|
|
390
|
+
return {
|
|
391
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Unknown action" }) }],
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const result = await crosspadInput(input);
|
|
396
|
+
return {
|
|
397
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
server.tool(
|
|
403
|
+
"crosspad_stats",
|
|
404
|
+
`Query runtime statistics from the running simulator. Returns:
|
|
405
|
+
- Platform capabilities (active flags)
|
|
406
|
+
- Pad state (16 pads: pressed, playing, note, channel, RGB color)
|
|
407
|
+
- Active/registered pad logic handlers
|
|
408
|
+
- Registered apps list
|
|
409
|
+
- Heap stats (SRAM/PSRAM free/total)
|
|
410
|
+
- Settings summary (brightness, theme, kit, audio engine)`,
|
|
411
|
+
{},
|
|
412
|
+
async () => {
|
|
413
|
+
const result = await crosspadStats();
|
|
414
|
+
return {
|
|
415
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
server.tool(
|
|
421
|
+
"crosspad_settings",
|
|
422
|
+
`Read or write CrossPad settings on the running simulator.
|
|
423
|
+
|
|
424
|
+
Read: action="get", category="all"|"display"|"keypad"|"vibration"|"wireless"|"audio"|"system"
|
|
425
|
+
Write: action="set", key="<setting_key>", value=<number> (booleans: 0/1)
|
|
426
|
+
|
|
427
|
+
Writable keys:
|
|
428
|
+
lcd_brightness (0-255), rgb_brightness (0-255), theme_color (0-255), perf_stats_flags (bitmask)
|
|
429
|
+
kit (0-255), audio_engine (0/1)
|
|
430
|
+
keypad.enable, keypad.inactive_lights, keypad.eco_mode, keypad.send_stm/ble/usb/cc (0/1)
|
|
431
|
+
vibration.enable, vibration.on_touch, vibration.in_min/max, vibration.out_min/max (0-255)
|
|
432
|
+
master_fx.mute (0/1), master_fx.in_volume, master_fx.out_volume (0-100)
|
|
433
|
+
|
|
434
|
+
Changes are auto-saved to ~/.crosspad/preferences.json.`,
|
|
435
|
+
{
|
|
436
|
+
action: z
|
|
437
|
+
.enum(["get", "set"])
|
|
438
|
+
.describe("Read or write settings"),
|
|
439
|
+
category: z
|
|
440
|
+
.string()
|
|
441
|
+
.default("all")
|
|
442
|
+
.describe("For get: all, display, keypad, vibration, wireless, audio, system"),
|
|
443
|
+
key: z
|
|
444
|
+
.string()
|
|
445
|
+
.optional()
|
|
446
|
+
.describe("For set: dotted key name (e.g. 'lcd_brightness', 'keypad.eco_mode')"),
|
|
447
|
+
value: z
|
|
448
|
+
.number()
|
|
449
|
+
.optional()
|
|
450
|
+
.describe("For set: numeric value (booleans: 0=false, 1=true)"),
|
|
451
|
+
},
|
|
452
|
+
async ({ action, category, key, value }) => {
|
|
453
|
+
if (action === "get") {
|
|
454
|
+
const result = await crosspadSettingsGet(category);
|
|
455
|
+
return {
|
|
456
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
457
|
+
};
|
|
458
|
+
} else {
|
|
459
|
+
if (!key || value === undefined) {
|
|
460
|
+
return {
|
|
461
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: "key and value required for set" }) }],
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const result = await crosspadSettingsSet(key, value);
|
|
465
|
+
return {
|
|
466
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
473
|
+
// START
|
|
474
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
475
|
+
|
|
476
|
+
async function main() {
|
|
477
|
+
const transport = new StdioServerTransport();
|
|
478
|
+
await server.connect(transport);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
main().catch((err) => {
|
|
482
|
+
console.error("MCP server failed:", err);
|
|
483
|
+
process.exit(1);
|
|
484
|
+
});
|