@srmorete/mobile-device-mcp 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 +21 -0
- package/README.md +117 -0
- package/bin/cli.js +2 -0
- package/drivers/android/app-debug-androidTest.apk +0 -0
- package/drivers/android/app-debug.apk +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.o +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/Project/arm64-apple-ios-simulator.swiftsourceinfo +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/Project/x86_64-apple-ios-simulator.swiftsourceinfo +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/arm64-apple-ios-simulator.abi.json +9 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/arm64-apple-ios-simulator.swiftmodule +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/x86_64-apple-ios-simulator.abi.json +9 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/x86_64-apple-ios-simulator.swiftmodule +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.o +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/Project/arm64-apple-ios-simulator.swiftsourceinfo +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/Project/x86_64-apple-ios-simulator.swiftsourceinfo +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/arm64-apple-ios-simulator.abi.json +9 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/arm64-apple-ios-simulator.swiftmodule +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/x86_64-apple-ios-simulator.abi.json +9 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/x86_64-apple-ios-simulator.swiftmodule +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/PkgInfo +1 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/UITreeServerApp +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/UITreeServerApp.debug.dylib +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/_CodeSignature/CodeResources +128 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/__preview.dylib +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/Project/arm64-apple-ios-simulator.swiftsourceinfo +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/Project/x86_64-apple-ios-simulator.swiftsourceinfo +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/arm64-apple-ios-simulator.abi.json +9 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/arm64-apple-ios-simulator.swiftmodule +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/x86_64-apple-ios-simulator.abi.json +9 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/x86_64-apple-ios-simulator.swiftmodule +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/Testing.framework/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/Testing.framework/Testing +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/Testing.framework/_CodeSignature/CodeResources +168 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/Testing.framework/version.plist +18 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTAutomationSupport.framework/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTAutomationSupport.framework/XCTAutomationSupport +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTAutomationSupport.framework/_CodeSignature/CodeResources +113 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTAutomationSupport.framework/version.plist +18 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTest.framework/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTest.framework/XCTest +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTest.framework/_CodeSignature/CodeResources +817 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTest.framework/version.plist +18 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestCore.framework/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestCore.framework/XCTestCore +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestCore.framework/_CodeSignature/CodeResources +113 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestCore.framework/version.plist +18 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestSupport.framework/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestSupport.framework/XCTestSupport +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestSupport.framework/_CodeSignature/CodeResources +113 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestSupport.framework/version.plist +18 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUIAutomation.framework/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUIAutomation.framework/XCUIAutomation +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUIAutomation.framework/_CodeSignature/CodeResources +432 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUIAutomation.framework/version.plist +18 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUnit.framework/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUnit.framework/XCUnit +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUnit.framework/_CodeSignature/CodeResources +113 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUnit.framework/version.plist +18 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/libXCTestSwiftSupport.dylib +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Info.plist +254 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/PkgInfo +1 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/PlugIns/UITreeServerUITests.xctest/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/PlugIns/UITreeServerUITests.xctest/UITreeServerUITests +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/PlugIns/UITreeServerUITests.xctest/_CodeSignature/CodeResources +101 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/UITreeServerUITests-Runner +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/_CodeSignature/CodeResources +458 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/Project/arm64-apple-ios-simulator.swiftsourceinfo +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/Project/x86_64-apple-ios-simulator.swiftsourceinfo +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/arm64-apple-ios-simulator.abi.json +9 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/arm64-apple-ios-simulator.swiftmodule +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/x86_64-apple-ios-simulator.abi.json +9 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/x86_64-apple-ios-simulator.swiftmodule +0 -0
- package/drivers/ios/UITreeServerUITests_iphonesimulator26.2-arm64-x86_64.xctestrun +135 -0
- package/drivers/ios/UITreeServerUITests_iphonesimulator26.2-arm64.xctestrun +135 -0
- package/package.json +32 -0
- package/src/filter/filter.ts +393 -0
- package/src/filter/index.ts +42 -0
- package/src/filter/types.ts +70 -0
- package/src/server/bootstrap.ts +367 -0
- package/src/server/devices.ts +262 -0
- package/src/server/index.ts +41 -0
- package/src/server/ports.ts +190 -0
- package/src/server/proxy.ts +119 -0
- package/src/server/tools.ts +303 -0
- package/src/server/types.ts +22 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { readdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { randomBytes } from "crypto";
|
|
4
|
+
import type { Subprocess } from "bun";
|
|
5
|
+
import type { RegisteredDevice, DeviceType } from "./types.js";
|
|
6
|
+
import {
|
|
7
|
+
getDevice,
|
|
8
|
+
setDevice,
|
|
9
|
+
removeDevice,
|
|
10
|
+
cleanupDevice,
|
|
11
|
+
detectPlatform,
|
|
12
|
+
} from "./devices.js";
|
|
13
|
+
import {
|
|
14
|
+
allocateAndroidPort,
|
|
15
|
+
allocateIOSPort,
|
|
16
|
+
addPendingPort,
|
|
17
|
+
releasePendingPort,
|
|
18
|
+
unlockPort,
|
|
19
|
+
cleanupStalePorts,
|
|
20
|
+
killPortListener,
|
|
21
|
+
} from "./ports.js";
|
|
22
|
+
|
|
23
|
+
const HEALTH_POLL_INTERVAL = 500;
|
|
24
|
+
const HEALTH_TIMEOUT = 45_000;
|
|
25
|
+
|
|
26
|
+
// Drivers root relative to project
|
|
27
|
+
const PROJECT_ROOT = join(import.meta.dir, "..", "..");
|
|
28
|
+
const DRIVERS_ANDROID = join(PROJECT_ROOT, "drivers", "android");
|
|
29
|
+
const DRIVERS_IOS_SIM = join(PROJECT_ROOT, "drivers", "ios");
|
|
30
|
+
const DRIVERS_IOS_DEVICE = join(PROJECT_ROOT, "drivers", "ios-device");
|
|
31
|
+
|
|
32
|
+
// ── Promise chains for per-device serialization ──
|
|
33
|
+
const bootstrapChains = new Map<string, Promise<void>>();
|
|
34
|
+
|
|
35
|
+
export async function ensureDevice(deviceId: string): Promise<RegisteredDevice> {
|
|
36
|
+
// Fast path: already registered — but verify server is still alive
|
|
37
|
+
const existing = getDevice(deviceId);
|
|
38
|
+
if (existing) {
|
|
39
|
+
if (existing.serverProcess.exitCode !== null) {
|
|
40
|
+
const old = removeDevice(deviceId);
|
|
41
|
+
if (old) await cleanupDevice(old);
|
|
42
|
+
} else {
|
|
43
|
+
return existing;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Serialize bootstrap per device
|
|
48
|
+
const chain = (bootstrapChains.get(deviceId) ?? Promise.resolve()).catch(() => {});
|
|
49
|
+
const next = chain.then(async () => {
|
|
50
|
+
// Re-check after waiting (a prior bootstrap may have succeeded)
|
|
51
|
+
const found = getDevice(deviceId);
|
|
52
|
+
if (found) return;
|
|
53
|
+
await bootstrapDevice(deviceId);
|
|
54
|
+
});
|
|
55
|
+
bootstrapChains.set(deviceId, next);
|
|
56
|
+
await next;
|
|
57
|
+
|
|
58
|
+
const device = getDevice(deviceId);
|
|
59
|
+
if (!device) throw new Error(`Bootstrap failed for device ${deviceId}`);
|
|
60
|
+
return device;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function bootstrapDevice(deviceId: string): Promise<void> {
|
|
64
|
+
const { platform, deviceType } = await detectPlatform(deviceId);
|
|
65
|
+
|
|
66
|
+
if (platform === "android") {
|
|
67
|
+
await bootstrapAndroid(deviceId);
|
|
68
|
+
} else {
|
|
69
|
+
await bootstrapIOS(deviceId, deviceType!);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Android bootstrap ──
|
|
74
|
+
|
|
75
|
+
async function bootstrapAndroid(deviceId: string): Promise<void> {
|
|
76
|
+
// Clean up stale state from a previous MCP instance (e.g. after Claude Code restart)
|
|
77
|
+
await cleanupStaleAndroid(deviceId);
|
|
78
|
+
|
|
79
|
+
const port = await allocateAndroidPort();
|
|
80
|
+
const authToken = randomBytes(32).toString("hex");
|
|
81
|
+
addPendingPort(port);
|
|
82
|
+
|
|
83
|
+
let serverProcess: Subprocess | undefined;
|
|
84
|
+
let cdpPort: number | undefined;
|
|
85
|
+
try {
|
|
86
|
+
// Install APKs
|
|
87
|
+
const apks = readdirSync(DRIVERS_ANDROID).filter((f) => f.endsWith(".apk"));
|
|
88
|
+
for (const apk of apks) {
|
|
89
|
+
const proc = Bun.spawn(["adb", "-s", deviceId, "install", "-r", "-g", join(DRIVERS_ANDROID, apk)], {
|
|
90
|
+
stdout: "pipe",
|
|
91
|
+
stderr: "pipe",
|
|
92
|
+
});
|
|
93
|
+
await proc.exited;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Forward host port to device server
|
|
97
|
+
await runAdb(deviceId, "forward", `tcp:${port}`, `tcp:${port}`);
|
|
98
|
+
|
|
99
|
+
// CDP forwarding: route device:9222 → host:cdpPort → device Chrome abstract socket
|
|
100
|
+
// ADB daemon bypasses SELinux app-to-app restrictions (CdpBridge.kt couldn't)
|
|
101
|
+
cdpPort = await setupCdpForwarding(deviceId);
|
|
102
|
+
|
|
103
|
+
// Write auth token to device file (avoids exposing in process args / ps output)
|
|
104
|
+
await runAdb(deviceId, "shell", `echo -n ${authToken} > /data/local/tmp/.mds_auth_${port}`);
|
|
105
|
+
|
|
106
|
+
// Spawn instrumentation
|
|
107
|
+
serverProcess = Bun.spawn(
|
|
108
|
+
[
|
|
109
|
+
"adb", "-s", deviceId, "shell", "am", "instrument", "-w", "-r",
|
|
110
|
+
"-e", "class", "dev.uitreeserver.UITreeServer#startServer",
|
|
111
|
+
"-e", "port", String(port),
|
|
112
|
+
"dev.uitreeserver.test/androidx.test.runner.AndroidJUnitRunner",
|
|
113
|
+
],
|
|
114
|
+
{ stdout: "ignore", stderr: "ignore" },
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Health poll
|
|
118
|
+
await healthPoll(port, serverProcess);
|
|
119
|
+
|
|
120
|
+
// Register
|
|
121
|
+
setDevice({
|
|
122
|
+
id: deviceId,
|
|
123
|
+
platform: "android",
|
|
124
|
+
port,
|
|
125
|
+
authToken,
|
|
126
|
+
serverProcess,
|
|
127
|
+
});
|
|
128
|
+
} catch (err) {
|
|
129
|
+
// Cleanup on failure
|
|
130
|
+
if (serverProcess) {
|
|
131
|
+
try { serverProcess.kill(9); } catch { /* */ }
|
|
132
|
+
}
|
|
133
|
+
try { await runAdb(deviceId, "forward", "--remove", `tcp:${port}`); } catch { /* */ }
|
|
134
|
+
if (cdpPort) {
|
|
135
|
+
try { await runAdb(deviceId, "forward", "--remove", `tcp:${cdpPort}`); } catch { /* */ }
|
|
136
|
+
}
|
|
137
|
+
try { await runAdb(deviceId, "reverse", "--remove", "tcp:9222"); } catch { /* */ }
|
|
138
|
+
try { await runAdb(deviceId, "shell", `rm -f /data/local/tmp/.mds_auth_${port}`); } catch { /* */ }
|
|
139
|
+
throw err;
|
|
140
|
+
} finally {
|
|
141
|
+
releasePendingPort(port);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function cleanupStaleAndroid(deviceId: string): Promise<void> {
|
|
146
|
+
// Kill any leftover instrumentation from a previous MCP instance
|
|
147
|
+
await runAdb(deviceId, "shell", "am", "force-stop", "dev.uitreeserver.test");
|
|
148
|
+
// Remove stale CDP reverse (device:9222 → host)
|
|
149
|
+
try { await runAdb(deviceId, "reverse", "--remove", "tcp:9222"); } catch { /* no reverse to remove */ }
|
|
150
|
+
// Remove all stale adb forwards for this device
|
|
151
|
+
try {
|
|
152
|
+
const proc = Bun.spawn(["adb", "-s", deviceId, "forward", "--list"], { stdout: "pipe", stderr: "pipe" });
|
|
153
|
+
const output = await new Response(proc.stdout).text();
|
|
154
|
+
await proc.exited;
|
|
155
|
+
for (const line of output.split("\n")) {
|
|
156
|
+
// adb forward --list returns ALL devices; only remove ours
|
|
157
|
+
if (!line.startsWith(deviceId + " ")) continue;
|
|
158
|
+
const match = line.match(/^\S+\s+(tcp:\d+)/);
|
|
159
|
+
if (match) {
|
|
160
|
+
await runAdb(deviceId, "forward", "--remove", match[1]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch { /* adb not available */ }
|
|
164
|
+
// Clean up any leftover auth token files
|
|
165
|
+
await runAdb(deviceId, "shell", "rm -f /data/local/tmp/.mds_auth_*");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function setupCdpForwarding(deviceId: string): Promise<number> {
|
|
169
|
+
// Let ADB pick a free host port (tcp:0) that tunnels to Chrome's abstract socket
|
|
170
|
+
const fwdProc = Bun.spawn(
|
|
171
|
+
["adb", "-s", deviceId, "forward", "tcp:0", "localabstract:chrome_devtools_remote"],
|
|
172
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
173
|
+
);
|
|
174
|
+
const cdpPort = parseInt((await new Response(fwdProc.stdout).text()).trim(), 10);
|
|
175
|
+
await fwdProc.exited;
|
|
176
|
+
if (isNaN(cdpPort)) {
|
|
177
|
+
throw new Error("Failed to allocate CDP forward port");
|
|
178
|
+
}
|
|
179
|
+
// Reverse: device:9222 → host:cdpPort (so on-device CdpClient reaches Chrome)
|
|
180
|
+
try {
|
|
181
|
+
await runAdb(deviceId, "reverse", "tcp:9222", `tcp:${cdpPort}`);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
try { await runAdb(deviceId, "forward", "--remove", `tcp:${cdpPort}`); } catch { /* */ }
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
return cdpPort;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function runAdb(deviceId: string, ...args: string[]): Promise<void> {
|
|
190
|
+
const proc = Bun.spawn(["adb", "-s", deviceId, ...args], {
|
|
191
|
+
stdout: "pipe",
|
|
192
|
+
stderr: "pipe",
|
|
193
|
+
});
|
|
194
|
+
await proc.exited;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── iOS bootstrap ──
|
|
198
|
+
|
|
199
|
+
async function bootstrapIOS(deviceId: string, deviceType: DeviceType): Promise<void> {
|
|
200
|
+
// Clean up stale state from a previous MCP instance (e.g. after Claude Code restart)
|
|
201
|
+
await cleanupStalePorts();
|
|
202
|
+
|
|
203
|
+
const port = await allocateIOSPort();
|
|
204
|
+
const authToken = randomBytes(32).toString("hex");
|
|
205
|
+
addPendingPort(port);
|
|
206
|
+
|
|
207
|
+
let serverProcess: Subprocess | undefined;
|
|
208
|
+
let tunnelProcess: Subprocess | undefined;
|
|
209
|
+
try {
|
|
210
|
+
if (deviceType === "simulator") {
|
|
211
|
+
serverProcess = await bootstrapIOSSimulator(deviceId, port, authToken);
|
|
212
|
+
} else {
|
|
213
|
+
const result = await bootstrapIOSDevice(deviceId, port, authToken);
|
|
214
|
+
serverProcess = result.serverProcess;
|
|
215
|
+
tunnelProcess = result.tunnelProcess;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Health poll
|
|
219
|
+
await healthPoll(port, serverProcess);
|
|
220
|
+
|
|
221
|
+
// Register
|
|
222
|
+
setDevice({
|
|
223
|
+
id: deviceId,
|
|
224
|
+
platform: "ios",
|
|
225
|
+
deviceType,
|
|
226
|
+
port,
|
|
227
|
+
authToken,
|
|
228
|
+
serverProcess,
|
|
229
|
+
tunnelProcess,
|
|
230
|
+
});
|
|
231
|
+
} catch (err) {
|
|
232
|
+
// Cleanup on failure
|
|
233
|
+
if (serverProcess) {
|
|
234
|
+
try { serverProcess.kill(9); } catch { /* */ }
|
|
235
|
+
}
|
|
236
|
+
if (tunnelProcess) {
|
|
237
|
+
try { tunnelProcess.kill(9); } catch { /* */ }
|
|
238
|
+
}
|
|
239
|
+
// For simulators, the spawn handle kill above doesn't kill the real server —
|
|
240
|
+
// kill whatever is actually listening on the port to prevent orphans.
|
|
241
|
+
if (deviceType === "simulator") {
|
|
242
|
+
await killPortListener(port);
|
|
243
|
+
}
|
|
244
|
+
unlockPort(port);
|
|
245
|
+
throw err;
|
|
246
|
+
} finally {
|
|
247
|
+
releasePendingPort(port);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── iOS simulator: simctl install + spawn (bypasses xcodebuild orchestration) ──
|
|
252
|
+
|
|
253
|
+
async function bootstrapIOSSimulator(deviceId: string, port: number, authToken: string): Promise<Subprocess> {
|
|
254
|
+
const buildDir = join(DRIVERS_IOS_SIM, "Debug-iphonesimulator");
|
|
255
|
+
const runnerApp = readdirSync(buildDir).find((f) => f.endsWith("-Runner.app"));
|
|
256
|
+
if (!runnerApp) {
|
|
257
|
+
throw new Error("No Runner.app found in iOS simulator drivers");
|
|
258
|
+
}
|
|
259
|
+
const runnerAppPath = join(buildDir, runnerApp);
|
|
260
|
+
const binaryName = runnerApp.replace(/\.app$/, "");
|
|
261
|
+
|
|
262
|
+
// Read bundle ID from Info.plist
|
|
263
|
+
const plistProc = Bun.spawn(
|
|
264
|
+
["/usr/libexec/PlistBuddy", "-c", "Print :CFBundleIdentifier", join(runnerAppPath, "Info.plist")],
|
|
265
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
266
|
+
);
|
|
267
|
+
const bundleId = (await new Response(plistProc.stdout).text()).trim();
|
|
268
|
+
await plistProc.exited;
|
|
269
|
+
if (!bundleId) {
|
|
270
|
+
throw new Error("Could not read bundle ID from Runner.app Info.plist");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Install the app on the simulator
|
|
274
|
+
const installProc = Bun.spawn(
|
|
275
|
+
["xcrun", "simctl", "install", deviceId, runnerAppPath],
|
|
276
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
277
|
+
);
|
|
278
|
+
const installExit = await installProc.exited;
|
|
279
|
+
if (installExit !== 0) {
|
|
280
|
+
const stderr = await new Response(installProc.stderr).text();
|
|
281
|
+
throw new Error(`simctl install failed: ${stderr}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Get installed app container path
|
|
285
|
+
const containerProc = Bun.spawn(
|
|
286
|
+
["xcrun", "simctl", "get_app_container", deviceId, bundleId],
|
|
287
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
288
|
+
);
|
|
289
|
+
const containerPath = (await new Response(containerProc.stdout).text()).trim();
|
|
290
|
+
await containerProc.exited;
|
|
291
|
+
if (!containerPath) {
|
|
292
|
+
throw new Error("Could not get app container path after install");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Spawn the runner binary inside the simulator (no foreground, no SpringBoard)
|
|
296
|
+
return Bun.spawn(
|
|
297
|
+
["xcrun", "simctl", "spawn", deviceId, join(containerPath, binaryName)],
|
|
298
|
+
{
|
|
299
|
+
stdout: "ignore",
|
|
300
|
+
stderr: "ignore",
|
|
301
|
+
env: {
|
|
302
|
+
...process.env,
|
|
303
|
+
SIMCTL_CHILD_PORT: String(port),
|
|
304
|
+
SIMCTL_CHILD_AUTH_TOKEN: authToken,
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── iOS real device: xcodebuild (still required for testmanagerd handshake) ──
|
|
311
|
+
|
|
312
|
+
async function bootstrapIOSDevice(
|
|
313
|
+
deviceId: string, port: number, authToken: string,
|
|
314
|
+
): Promise<{ serverProcess: Subprocess; tunnelProcess: Subprocess }> {
|
|
315
|
+
const xctestrunFile = readdirSync(DRIVERS_IOS_DEVICE).find((f) => f.endsWith(".xctestrun"));
|
|
316
|
+
if (!xctestrunFile) {
|
|
317
|
+
throw new Error("No .xctestrun driver found for iOS device");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const serverProcess = Bun.spawn(
|
|
321
|
+
[
|
|
322
|
+
"xcodebuild", "test-without-building",
|
|
323
|
+
"-xctestrun", join(DRIVERS_IOS_DEVICE, xctestrunFile),
|
|
324
|
+
"-destination", `platform=iOS,id=${deviceId}`,
|
|
325
|
+
"-parallel-testing-enabled", "NO",
|
|
326
|
+
"-allowProvisioningUpdates",
|
|
327
|
+
],
|
|
328
|
+
{
|
|
329
|
+
stdout: "ignore",
|
|
330
|
+
stderr: "ignore",
|
|
331
|
+
env: {
|
|
332
|
+
...process.env,
|
|
333
|
+
TEST_RUNNER_PORT: String(port),
|
|
334
|
+
TEST_RUNNER_AUTH_TOKEN: authToken,
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const tunnelProcess = Bun.spawn(
|
|
340
|
+
["iproxy", String(port), String(port), "-u", deviceId, "-l", "127.0.0.1"],
|
|
341
|
+
{ stdout: "ignore", stderr: "ignore" },
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
return { serverProcess, tunnelProcess };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── Health poll ──
|
|
348
|
+
|
|
349
|
+
async function healthPoll(port: number, serverProcess: Subprocess): Promise<void> {
|
|
350
|
+
const deadline = Date.now() + HEALTH_TIMEOUT;
|
|
351
|
+
while (Date.now() < deadline) {
|
|
352
|
+
try {
|
|
353
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`);
|
|
354
|
+
if (res.ok) return;
|
|
355
|
+
} catch {
|
|
356
|
+
// Connection not ready yet
|
|
357
|
+
}
|
|
358
|
+
// Check if the process died
|
|
359
|
+
if (serverProcess.exitCode !== null) {
|
|
360
|
+
throw new Error(`Server process exited with code ${serverProcess.exitCode} before becoming healthy`);
|
|
361
|
+
}
|
|
362
|
+
await Bun.sleep(HEALTH_POLL_INTERVAL);
|
|
363
|
+
}
|
|
364
|
+
// Timeout: kill and throw
|
|
365
|
+
try { serverProcess.kill(9); } catch { /* */ }
|
|
366
|
+
throw new Error(`Health check timed out after ${HEALTH_TIMEOUT}ms on port ${port}`);
|
|
367
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { unlinkSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import type { RegisteredDevice, DiscoveredDevice, Platform, DeviceType } from "./types.js";
|
|
5
|
+
import { killPortListener } from "./ports.js";
|
|
6
|
+
|
|
7
|
+
const LOCK_DIR = join(homedir(), ".mdms", "ports");
|
|
8
|
+
|
|
9
|
+
// ── Device registry ──
|
|
10
|
+
|
|
11
|
+
const registry = new Map<string, RegisteredDevice>();
|
|
12
|
+
|
|
13
|
+
export function getDevice(id: string): RegisteredDevice | undefined {
|
|
14
|
+
return registry.get(id);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function setDevice(device: RegisteredDevice): void {
|
|
18
|
+
registry.set(device.id, device);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function removeDevice(id: string): RegisteredDevice | undefined {
|
|
22
|
+
const device = registry.get(id);
|
|
23
|
+
if (device) registry.delete(id);
|
|
24
|
+
return device;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function allDevices(): RegisteredDevice[] {
|
|
28
|
+
return Array.from(registry.values());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Discovery ──
|
|
32
|
+
|
|
33
|
+
async function runCommand(cmd: string[]): Promise<string> {
|
|
34
|
+
const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
35
|
+
const text = await new Response(proc.stdout).text();
|
|
36
|
+
await proc.exited;
|
|
37
|
+
return text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function discoverAndroid(): Promise<DiscoveredDevice[]> {
|
|
41
|
+
try {
|
|
42
|
+
const output = await runCommand(["adb", "devices", "-l"]);
|
|
43
|
+
const devices: DiscoveredDevice[] = [];
|
|
44
|
+
for (const line of output.split("\n")) {
|
|
45
|
+
const trimmed = line.trim();
|
|
46
|
+
if (!trimmed || trimmed.startsWith("List of")) continue;
|
|
47
|
+
// Format: <serial> <state> <info...>
|
|
48
|
+
const parts = trimmed.split(/\s+/);
|
|
49
|
+
if (parts.length < 2) continue;
|
|
50
|
+
const id = parts[0];
|
|
51
|
+
const state = parts[1];
|
|
52
|
+
let name = id;
|
|
53
|
+
for (const part of parts.slice(2)) {
|
|
54
|
+
if (part.startsWith("model:")) {
|
|
55
|
+
name = part.slice("model:".length);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
devices.push({ id, platform: "android", name, state });
|
|
60
|
+
}
|
|
61
|
+
return devices;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error("Android discovery failed:", err);
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function discoverIOSSimulators(): Promise<DiscoveredDevice[]> {
|
|
69
|
+
try {
|
|
70
|
+
const output = await runCommand(["xcrun", "simctl", "list", "devices", "booted", "-j"]);
|
|
71
|
+
const json = JSON.parse(output);
|
|
72
|
+
const devices: DiscoveredDevice[] = [];
|
|
73
|
+
const runtimes = json.devices || {};
|
|
74
|
+
for (const runtime of Object.keys(runtimes)) {
|
|
75
|
+
for (const device of runtimes[runtime]) {
|
|
76
|
+
if (device.state !== "Booted") continue;
|
|
77
|
+
devices.push({
|
|
78
|
+
id: device.udid,
|
|
79
|
+
platform: "ios",
|
|
80
|
+
name: device.name,
|
|
81
|
+
state: device.state,
|
|
82
|
+
deviceType: "simulator",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return devices;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error("iOS simulator discovery failed:", err);
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function discoverIOSRealDevices(): Promise<DiscoveredDevice[]> {
|
|
94
|
+
try {
|
|
95
|
+
const output = await runCommand(["xcrun", "devicectl", "list", "devices", "--json-output", "/dev/stdout"]);
|
|
96
|
+
const json = JSON.parse(output);
|
|
97
|
+
const devices: DiscoveredDevice[] = [];
|
|
98
|
+
const result = json.result?.devices || [];
|
|
99
|
+
for (const device of result) {
|
|
100
|
+
// Only include USB-connected devices
|
|
101
|
+
if (device.connectionProperties?.transportType !== "wired") continue;
|
|
102
|
+
devices.push({
|
|
103
|
+
id: device.identifier,
|
|
104
|
+
platform: "ios",
|
|
105
|
+
name: device.deviceProperties?.name || device.identifier,
|
|
106
|
+
state: "connected",
|
|
107
|
+
deviceType: "device",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return devices;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error("iOS real device discovery failed:", err);
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function discoverDevices(): Promise<DiscoveredDevice[]> {
|
|
118
|
+
const [android, iosSim, iosDevice] = await Promise.all([
|
|
119
|
+
discoverAndroid(),
|
|
120
|
+
discoverIOSSimulators(),
|
|
121
|
+
discoverIOSRealDevices(),
|
|
122
|
+
]);
|
|
123
|
+
return [...android, ...iosSim, ...iosDevice];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type DetectedPlatformInfo = {
|
|
127
|
+
platform: Platform;
|
|
128
|
+
deviceType?: DeviceType;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export async function detectPlatform(deviceId: string): Promise<DetectedPlatformInfo> {
|
|
132
|
+
// Check Android first
|
|
133
|
+
try {
|
|
134
|
+
const output = await runCommand(["adb", "devices"]);
|
|
135
|
+
for (const line of output.split("\n")) {
|
|
136
|
+
const serial = line.split("\t")[0];
|
|
137
|
+
if (serial === deviceId) {
|
|
138
|
+
return { platform: "android" };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch { /* adb not available */ }
|
|
142
|
+
|
|
143
|
+
// Check iOS simulators
|
|
144
|
+
try {
|
|
145
|
+
const output = await runCommand(["xcrun", "simctl", "list", "devices", "booted", "-j"]);
|
|
146
|
+
const json = JSON.parse(output);
|
|
147
|
+
const runtimes = json.devices || {};
|
|
148
|
+
for (const runtime of Object.keys(runtimes)) {
|
|
149
|
+
for (const device of runtimes[runtime]) {
|
|
150
|
+
if (device.udid === deviceId && device.state === "Booted") {
|
|
151
|
+
return { platform: "ios", deviceType: "simulator" };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} catch { /* simctl not available */ }
|
|
156
|
+
|
|
157
|
+
// Check iOS real devices
|
|
158
|
+
try {
|
|
159
|
+
const output = await runCommand(["xcrun", "devicectl", "list", "devices", "--json-output", "/dev/stdout"]);
|
|
160
|
+
const json = JSON.parse(output);
|
|
161
|
+
const result = json.result?.devices || [];
|
|
162
|
+
for (const device of result) {
|
|
163
|
+
if (device.identifier === deviceId && device.connectionProperties?.transportType === "wired") {
|
|
164
|
+
return { platform: "ios", deviceType: "device" };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch { /* devicectl not available */ }
|
|
168
|
+
|
|
169
|
+
throw new Error(`Device ${deviceId} not found in adb, simctl, or devicectl`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Cleanup ──
|
|
173
|
+
|
|
174
|
+
export async function cleanupDevice(device: RegisteredDevice): Promise<void> {
|
|
175
|
+
// Kill server process (SIGKILL, try process group first)
|
|
176
|
+
try {
|
|
177
|
+
if (device.serverProcess.pid && device.serverProcess.pid > 0) {
|
|
178
|
+
process.kill(-device.serverProcess.pid, "SIGKILL");
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
try {
|
|
182
|
+
device.serverProcess.kill(9);
|
|
183
|
+
} catch { /* already dead */ }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Kill tunnel process if present (iOS real devices)
|
|
187
|
+
if (device.tunnelProcess) {
|
|
188
|
+
try {
|
|
189
|
+
if (device.tunnelProcess.pid && device.tunnelProcess.pid > 0) {
|
|
190
|
+
process.kill(-device.tunnelProcess.pid, "SIGKILL");
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
try {
|
|
194
|
+
device.tunnelProcess.kill(9);
|
|
195
|
+
} catch { /* already dead */ }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Platform-specific cleanup
|
|
200
|
+
if (device.platform === "android") {
|
|
201
|
+
// Force-stop the test package
|
|
202
|
+
const forceStop = Bun.spawn(["adb", "-s", device.id, "shell", "am", "force-stop", "dev.uitreeserver.test"], {
|
|
203
|
+
stdout: "ignore",
|
|
204
|
+
stderr: "ignore",
|
|
205
|
+
});
|
|
206
|
+
await forceStop.exited;
|
|
207
|
+
|
|
208
|
+
// Remove auth token file from device
|
|
209
|
+
const rmAuth = Bun.spawn(
|
|
210
|
+
["adb", "-s", device.id, "shell", `rm -f /data/local/tmp/.mds_auth_${device.port}`],
|
|
211
|
+
{ stdout: "ignore", stderr: "ignore" },
|
|
212
|
+
);
|
|
213
|
+
await rmAuth.exited;
|
|
214
|
+
|
|
215
|
+
// Remove CDP reverse (device:9222 → host)
|
|
216
|
+
const rmReverse = Bun.spawn(
|
|
217
|
+
["adb", "-s", device.id, "reverse", "--remove", "tcp:9222"],
|
|
218
|
+
{ stdout: "ignore", stderr: "ignore" },
|
|
219
|
+
);
|
|
220
|
+
await rmReverse.exited;
|
|
221
|
+
|
|
222
|
+
// Remove ALL ADB forwards for this device (not just registered ports)
|
|
223
|
+
try {
|
|
224
|
+
const listProc = Bun.spawn(["adb", "-s", device.id, "forward", "--list"], { stdout: "pipe", stderr: "ignore" });
|
|
225
|
+
const output = await new Response(listProc.stdout).text();
|
|
226
|
+
await listProc.exited;
|
|
227
|
+
const removes: Promise<number>[] = [];
|
|
228
|
+
for (const line of output.split("\n")) {
|
|
229
|
+
// adb forward --list returns ALL devices; only remove ours
|
|
230
|
+
if (!line.startsWith(device.id + " ")) continue;
|
|
231
|
+
const match = line.match(/^\S+\s+(tcp:\d+)/);
|
|
232
|
+
if (match) {
|
|
233
|
+
removes.push(
|
|
234
|
+
Bun.spawn(["adb", "-s", device.id, "forward", "--remove", match[1]], {
|
|
235
|
+
stdout: "ignore", stderr: "ignore",
|
|
236
|
+
}).exited,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
await Promise.allSettled(removes);
|
|
241
|
+
} catch { /* adb not available */ }
|
|
242
|
+
} else if (device.platform === "ios") {
|
|
243
|
+
// For simulators, kill the actual server inside the simulator.
|
|
244
|
+
// serverProcess is just the simctl spawn handle — the real server
|
|
245
|
+
// runs under the simulator's launchd and survives handle death.
|
|
246
|
+
if (device.deviceType === "simulator") {
|
|
247
|
+
await killPortListener(device.port);
|
|
248
|
+
}
|
|
249
|
+
// Delete port lock file
|
|
250
|
+
try {
|
|
251
|
+
unlinkSync(join(LOCK_DIR, String(device.port)));
|
|
252
|
+
} catch { /* file may not exist */ }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function cleanupAll(): Promise<void> {
|
|
257
|
+
const devices = allDevices();
|
|
258
|
+
await Promise.allSettled(devices.map((device) => cleanupDevice(device)));
|
|
259
|
+
for (const device of devices) {
|
|
260
|
+
registry.delete(device.id);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { registerTools } from "./tools.js";
|
|
4
|
+
import { cleanupAll } from "./devices.js";
|
|
5
|
+
import pkg from "../../package.json";
|
|
6
|
+
|
|
7
|
+
// ── Shutdown ──
|
|
8
|
+
|
|
9
|
+
let shuttingDown = false;
|
|
10
|
+
|
|
11
|
+
async function shutdown(): Promise<void> {
|
|
12
|
+
if (shuttingDown) return;
|
|
13
|
+
shuttingDown = true;
|
|
14
|
+
console.error("Shutting down, cleaning up devices...");
|
|
15
|
+
await cleanupAll();
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
process.on("SIGINT", shutdown);
|
|
20
|
+
process.on("SIGTERM", shutdown);
|
|
21
|
+
|
|
22
|
+
// ── Main ──
|
|
23
|
+
|
|
24
|
+
async function main(): Promise<void> {
|
|
25
|
+
const server = new McpServer(
|
|
26
|
+
{ name: pkg.name, version: pkg.version },
|
|
27
|
+
{ capabilities: { tools: {} } },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
registerTools(server);
|
|
31
|
+
|
|
32
|
+
const transport = new StdioServerTransport();
|
|
33
|
+
await server.connect(transport);
|
|
34
|
+
console.error("MCP server running on stdio");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
main().catch(async (err) => {
|
|
38
|
+
console.error("Fatal error:", err);
|
|
39
|
+
await cleanupAll();
|
|
40
|
+
process.exit(1);
|
|
41
|
+
});
|