@ait-co/devtools 0.1.22 → 0.1.23
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.en.md +114 -4
- package/README.md +124 -4
- package/dist/in-app/index.d.ts +136 -0
- package/dist/in-app/index.d.ts.map +1 -0
- package/dist/in-app/index.js +163 -0
- package/dist/in-app/index.js.map +1 -0
- package/dist/mcp/cli.d.ts +20 -0
- package/dist/mcp/cli.d.ts.map +1 -0
- package/dist/mcp/cli.js +930 -0
- package/dist/mcp/cli.js.map +1 -0
- package/dist/mcp/server.d.ts +79 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +285 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/panel/index.d.ts.map +1 -1
- package/dist/panel/index.js +20 -4
- package/dist/panel/index.js.map +1 -1
- package/dist/unplugin/index.cjs +64 -20
- package/dist/unplugin/index.cjs.map +1 -1
- package/dist/unplugin/index.d.cts +11 -0
- package/dist/unplugin/index.d.cts.map +1 -1
- package/dist/unplugin/index.d.ts +11 -0
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +64 -20
- package/dist/unplugin/index.js.map +1 -1
- package/package.json +22 -2
package/dist/mcp/cli.js
ADDED
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { argv } from "node:process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { EventEmitter } from "node:events";
|
|
10
|
+
import { WebSocket } from "ws";
|
|
11
|
+
import { createServer } from "node:http";
|
|
12
|
+
import { randomBytes } from "node:crypto";
|
|
13
|
+
import { Tunnel, bin, install } from "cloudflared";
|
|
14
|
+
import qrcode from "qrcode-terminal";
|
|
15
|
+
//#region src/mcp/ait-chii-source.ts
|
|
16
|
+
function isObject$2(value) {
|
|
17
|
+
return typeof value === "object" && value !== null;
|
|
18
|
+
}
|
|
19
|
+
/** Narrows an `AIT.getSdkCallHistory` response, tolerating a missing array. */
|
|
20
|
+
function asSdkCallHistory(raw) {
|
|
21
|
+
if (isObject$2(raw) && Array.isArray(raw.calls)) return { calls: raw.calls };
|
|
22
|
+
return { calls: [] };
|
|
23
|
+
}
|
|
24
|
+
/** Narrows an `AIT.getMockState` response to an opaque record. */
|
|
25
|
+
function asMockState(raw) {
|
|
26
|
+
return isObject$2(raw) ? raw : {};
|
|
27
|
+
}
|
|
28
|
+
/** Narrows an `AIT.getOperationalEnvironment` response. */
|
|
29
|
+
function asOperationalEnvironment(raw) {
|
|
30
|
+
return {
|
|
31
|
+
environment: isObject$2(raw) && typeof raw.environment === "string" ? raw.environment : "unknown",
|
|
32
|
+
sdkVersion: isObject$2(raw) && typeof raw.sdkVersion === "string" ? raw.sdkVersion : null
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
var ChiiAitSource = class {
|
|
36
|
+
constructor(sender) {
|
|
37
|
+
this.sender = sender;
|
|
38
|
+
}
|
|
39
|
+
async get(method) {
|
|
40
|
+
const raw = await this.sender.sendCommand(method);
|
|
41
|
+
switch (method) {
|
|
42
|
+
case "AIT.getSdkCallHistory": return asSdkCallHistory(raw);
|
|
43
|
+
case "AIT.getMockState": return asMockState(raw);
|
|
44
|
+
case "AIT.getOperationalEnvironment": return asOperationalEnvironment(raw);
|
|
45
|
+
default: throw new Error(`Unknown AIT method: ${String(method)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/mcp/chii-connection.ts
|
|
51
|
+
/**
|
|
52
|
+
* Production `CdpConnection` backed by the local Chii relay.
|
|
53
|
+
*
|
|
54
|
+
* Topology (debug mode):
|
|
55
|
+
* phone target.js --WS--> Chii relay :9100 <--WS-- this connection
|
|
56
|
+
*
|
|
57
|
+
* The phone connects to the relay as a `target`; this module connects as a
|
|
58
|
+
* `client` (the role a CDP frontend would take) so CDP events the page emits
|
|
59
|
+
* (`Runtime.consoleAPICalled`, `Network.*`) flow back here. We buffer recent
|
|
60
|
+
* events in ring buffers the tool layer reads via `getBufferedEvents`.
|
|
61
|
+
*
|
|
62
|
+
* Node-only: imports `ws`. Never bundled into the browser/in-app entries.
|
|
63
|
+
*/
|
|
64
|
+
/** Max events retained per domain ring buffer. */
|
|
65
|
+
const DEFAULT_BUFFER_SIZE = 500;
|
|
66
|
+
function isObject$1(value) {
|
|
67
|
+
return typeof value === "object" && value !== null;
|
|
68
|
+
}
|
|
69
|
+
function parseInbound(raw) {
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(raw);
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (!isObject$1(parsed)) return null;
|
|
77
|
+
const message = {};
|
|
78
|
+
if (typeof parsed.id === "number") message.id = parsed.id;
|
|
79
|
+
if (typeof parsed.method === "string") message.method = parsed.method;
|
|
80
|
+
if ("params" in parsed) message.params = parsed.params;
|
|
81
|
+
if ("result" in parsed) message.result = parsed.result;
|
|
82
|
+
if (isObject$1(parsed.error) && typeof parsed.error.message === "string") message.error = { message: parsed.error.message };
|
|
83
|
+
return message;
|
|
84
|
+
}
|
|
85
|
+
const PHASE_1_EVENTS = [
|
|
86
|
+
"Runtime.consoleAPICalled",
|
|
87
|
+
"Network.requestWillBeSent",
|
|
88
|
+
"Network.responseReceived"
|
|
89
|
+
];
|
|
90
|
+
/**
|
|
91
|
+
* Production CDP connection. Polls the relay for the first attached target,
|
|
92
|
+
* opens a client websocket to it, enables Phase 1 domains, and buffers events.
|
|
93
|
+
*/
|
|
94
|
+
var ChiiCdpConnection = class {
|
|
95
|
+
relayBaseUrl;
|
|
96
|
+
bufferSize;
|
|
97
|
+
emitter = new EventEmitter();
|
|
98
|
+
buffers = /* @__PURE__ */ new Map();
|
|
99
|
+
targets = /* @__PURE__ */ new Map();
|
|
100
|
+
ws = null;
|
|
101
|
+
nextCommandId = 1;
|
|
102
|
+
/** In-flight enableDomains() promise — concurrent callers share it. */
|
|
103
|
+
enablingPromise = null;
|
|
104
|
+
/** Pending request→response commands keyed by CDP message id. */
|
|
105
|
+
pending = /* @__PURE__ */ new Map();
|
|
106
|
+
constructor(options) {
|
|
107
|
+
this.relayBaseUrl = options.relayBaseUrl.replace(/\/$/, "");
|
|
108
|
+
this.bufferSize = options.bufferSize ?? DEFAULT_BUFFER_SIZE;
|
|
109
|
+
for (const event of PHASE_1_EVENTS) this.buffers.set(event, []);
|
|
110
|
+
this.emitter.setMaxListeners(0);
|
|
111
|
+
}
|
|
112
|
+
/** Refresh the attached-target list from the relay's `GET /targets`. */
|
|
113
|
+
async refreshTargets() {
|
|
114
|
+
const res = await fetch(`${this.relayBaseUrl}/targets`);
|
|
115
|
+
if (!res.ok) throw new Error(`Chii relay /targets returned HTTP ${res.status} ${res.statusText}`);
|
|
116
|
+
const body = await res.json();
|
|
117
|
+
const list = isObject$1(body) && Array.isArray(body.targets) ? body.targets : [];
|
|
118
|
+
this.targets.clear();
|
|
119
|
+
for (const item of list) {
|
|
120
|
+
if (!isObject$1(item) || typeof item.id !== "string") continue;
|
|
121
|
+
this.targets.set(item.id, {
|
|
122
|
+
id: item.id,
|
|
123
|
+
title: typeof item.title === "string" ? item.title : "",
|
|
124
|
+
url: typeof item.url === "string" ? item.url : ""
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return [...this.targets.values()];
|
|
128
|
+
}
|
|
129
|
+
listTargets() {
|
|
130
|
+
return [...this.targets.values()];
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Connect a client websocket to the first attached target and enable Phase 1
|
|
134
|
+
* domains. Resolves once the socket is open and enable commands are sent.
|
|
135
|
+
*/
|
|
136
|
+
async enableDomains() {
|
|
137
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
|
|
138
|
+
if (this.enablingPromise) return this.enablingPromise;
|
|
139
|
+
this.enablingPromise = this._doEnableDomains().finally(() => {
|
|
140
|
+
this.enablingPromise = null;
|
|
141
|
+
});
|
|
142
|
+
return this.enablingPromise;
|
|
143
|
+
}
|
|
144
|
+
async _doEnableDomains() {
|
|
145
|
+
const target = (await this.refreshTargets())[0];
|
|
146
|
+
if (!target) throw new Error("No mini-app page attached to the Chii relay yet.");
|
|
147
|
+
const ws = new WebSocket(`${this.relayBaseUrl.replace(/^http/, "ws")}/client/${`devtools-mcp-${Date.now()}`}?target=${encodeURIComponent(target.id)}`);
|
|
148
|
+
this.ws = ws;
|
|
149
|
+
await new Promise((resolve, reject) => {
|
|
150
|
+
ws.once("open", () => resolve());
|
|
151
|
+
ws.once("error", (err) => reject(err));
|
|
152
|
+
});
|
|
153
|
+
ws.on("message", (data) => this.handleMessage(data.toString()));
|
|
154
|
+
this.sendFireAndForget("Runtime.enable");
|
|
155
|
+
this.sendFireAndForget("Network.enable");
|
|
156
|
+
this.sendFireAndForget("DOM.enable");
|
|
157
|
+
this.sendFireAndForget("Page.enable");
|
|
158
|
+
}
|
|
159
|
+
/** Fire-and-forget CDP message (used for `*.enable`, no result awaited). */
|
|
160
|
+
sendFireAndForget(method, params = {}) {
|
|
161
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
162
|
+
const id = this.nextCommandId++;
|
|
163
|
+
this.ws.send(JSON.stringify({
|
|
164
|
+
id,
|
|
165
|
+
method,
|
|
166
|
+
params
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Issue a CDP command and resolve with its result (Phase 2). Rejects on a CDP
|
|
171
|
+
* error frame or when no websocket is open (no page attached yet).
|
|
172
|
+
*/
|
|
173
|
+
send(method, params) {
|
|
174
|
+
return this.sendCommand(method, params ?? {});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Issue an arbitrary request→response command over the relay and resolve with
|
|
178
|
+
* its raw result. Both the typed CDP {@link send} and the AIT domain (Phase 3
|
|
179
|
+
* `AIT.*` methods, forwarded over the same Chii channel) build on this.
|
|
180
|
+
*/
|
|
181
|
+
sendCommand(method, params = {}) {
|
|
182
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return Promise.reject(/* @__PURE__ */ new Error("No mini-app page attached to the Chii relay yet. Call enableDomains() first."));
|
|
183
|
+
const id = this.nextCommandId++;
|
|
184
|
+
const ws = this.ws;
|
|
185
|
+
return new Promise((resolve, reject) => {
|
|
186
|
+
this.pending.set(id, {
|
|
187
|
+
resolve,
|
|
188
|
+
reject
|
|
189
|
+
});
|
|
190
|
+
ws.send(JSON.stringify({
|
|
191
|
+
id,
|
|
192
|
+
method,
|
|
193
|
+
params
|
|
194
|
+
}));
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
handleMessage(raw) {
|
|
198
|
+
const message = parseInbound(raw);
|
|
199
|
+
if (!message) return;
|
|
200
|
+
if (typeof message.id === "number" && this.pending.has(message.id)) {
|
|
201
|
+
const waiter = this.pending.get(message.id);
|
|
202
|
+
this.pending.delete(message.id);
|
|
203
|
+
if (waiter) if (message.error) waiter.reject(new Error(message.error.message));
|
|
204
|
+
else waiter.resolve(message.result);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (typeof message.method !== "string") return;
|
|
208
|
+
if (!this.buffers.has(message.method)) return;
|
|
209
|
+
const event = message.method;
|
|
210
|
+
const buffer = this.buffers.get(event);
|
|
211
|
+
if (!buffer) return;
|
|
212
|
+
buffer.push(message.params);
|
|
213
|
+
if (buffer.length > this.bufferSize) buffer.shift();
|
|
214
|
+
this.emitter.emit(event, message.params);
|
|
215
|
+
}
|
|
216
|
+
getBufferedEvents(event) {
|
|
217
|
+
return this.buffers.get(event) ?? [];
|
|
218
|
+
}
|
|
219
|
+
on(event, listener) {
|
|
220
|
+
this.emitter.on(event, listener);
|
|
221
|
+
return () => this.emitter.off(event, listener);
|
|
222
|
+
}
|
|
223
|
+
/** Close the relay client websocket and reject any in-flight commands. */
|
|
224
|
+
close() {
|
|
225
|
+
this.ws?.close();
|
|
226
|
+
this.ws = null;
|
|
227
|
+
for (const waiter of this.pending.values()) waiter.reject(/* @__PURE__ */ new Error("Chii relay connection closed."));
|
|
228
|
+
this.pending.clear();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/mcp/chii-relay.ts
|
|
233
|
+
/**
|
|
234
|
+
* Boots the local Chii relay server.
|
|
235
|
+
*
|
|
236
|
+
* Chii (liriliri/chii) is a chobitsu-based CDP relay that lets non-Chrome
|
|
237
|
+
* WebViews (iOS WKWebView / Android WebView — i.e. the Toss app) expose CDP.
|
|
238
|
+
* The relay accepts a `target` websocket from the phone's injected `target.js`
|
|
239
|
+
* and `client` websockets from CDP frontends (our MCP connection).
|
|
240
|
+
*
|
|
241
|
+
* Node-only: `chii` pulls in Koa + ws. Never bundled into the browser/in-app
|
|
242
|
+
* entries.
|
|
243
|
+
*/
|
|
244
|
+
const require = createRequire(import.meta.url);
|
|
245
|
+
function loadChiiServer() {
|
|
246
|
+
const mod = require("chii");
|
|
247
|
+
if (typeof mod === "object" && mod !== null && "start" in mod && typeof mod.start === "function") return mod;
|
|
248
|
+
throw new Error("chii server module did not expose start()");
|
|
249
|
+
}
|
|
250
|
+
/** Starts the Chii relay on the given port and resolves once listening. */
|
|
251
|
+
async function startChiiRelay(options = {}) {
|
|
252
|
+
const port = options.port ?? 9100;
|
|
253
|
+
const host = options.host ?? "127.0.0.1";
|
|
254
|
+
const httpServer = createServer();
|
|
255
|
+
await loadChiiServer().start({
|
|
256
|
+
server: httpServer,
|
|
257
|
+
domain: `${host}:${port}`,
|
|
258
|
+
port
|
|
259
|
+
});
|
|
260
|
+
await new Promise((resolve, reject) => {
|
|
261
|
+
httpServer.once("error", reject);
|
|
262
|
+
httpServer.listen(port, host, () => {
|
|
263
|
+
httpServer.off("error", reject);
|
|
264
|
+
resolve();
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
return {
|
|
268
|
+
port,
|
|
269
|
+
baseUrl: `http://${host}:${port}`,
|
|
270
|
+
close: () => new Promise((resolve) => {
|
|
271
|
+
httpServer.close(() => resolve());
|
|
272
|
+
})
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/mcp/tools.ts
|
|
277
|
+
/** Static MCP tool descriptors (name + JSONSchema) for the Phase 1 surface. */
|
|
278
|
+
const DEBUG_TOOL_DEFINITIONS = [
|
|
279
|
+
{
|
|
280
|
+
name: "list_console_messages",
|
|
281
|
+
description: "Lists recent console messages (console.log/warn/error/info) captured from the attached mini-app page over CDP (Runtime.consoleAPICalled). Read-only. Returns level, text, timestamp, and stringified args, oldest-first.",
|
|
282
|
+
inputSchema: {
|
|
283
|
+
type: "object",
|
|
284
|
+
properties: {},
|
|
285
|
+
required: []
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: "list_network_requests",
|
|
290
|
+
description: "Lists recent network requests (XHR/fetch) captured from the attached mini-app page over CDP (Network.requestWillBeSent + Network.responseReceived). Read-only. Returns url, method, status, and timing, oldest-first.",
|
|
291
|
+
inputSchema: {
|
|
292
|
+
type: "object",
|
|
293
|
+
properties: {},
|
|
294
|
+
required: []
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: "list_pages",
|
|
299
|
+
description: "Lists the mini-app page(s) the Chii relay currently sees attached, plus whether the cloudflared tunnel is up and the public wss relay URL the phone uses to attach. Call this first to confirm a page is attached before reading console/network.",
|
|
300
|
+
inputSchema: {
|
|
301
|
+
type: "object",
|
|
302
|
+
properties: {},
|
|
303
|
+
required: []
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: "get_dom_document",
|
|
308
|
+
description: "Returns the DOM tree of the attached mini-app page over CDP (DOM.getDocument). Read-only. Use for structural/layout regression diagnosis (e.g. confirming an element exists, inspecting attributes). Returns the document root node with children.",
|
|
309
|
+
inputSchema: {
|
|
310
|
+
type: "object",
|
|
311
|
+
properties: {},
|
|
312
|
+
required: []
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "take_snapshot",
|
|
317
|
+
description: "Captures a serialized snapshot of the attached page over CDP (DOMSnapshot.captureSnapshot). Read-only. Returns the documents + interned strings table for visual-regression diagnosis (e.g. checking computed CSS custom properties like --sat against the live layout).",
|
|
318
|
+
inputSchema: {
|
|
319
|
+
type: "object",
|
|
320
|
+
properties: {},
|
|
321
|
+
required: []
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: "take_screenshot",
|
|
326
|
+
description: "Captures a PNG screenshot of the attached mini-app page over CDP (Page.captureScreenshot) so the agent can see the phone screen directly. Read-only. Returns an image content block.",
|
|
327
|
+
inputSchema: {
|
|
328
|
+
type: "object",
|
|
329
|
+
properties: {},
|
|
330
|
+
required: []
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
name: "AIT.getSdkCallHistory",
|
|
335
|
+
description: "Returns the recent Apps In Toss SDK call trace (method, args, result/error, timestamp) that raw CDP cannot observe. Read-only. Use to confirm an SDK call fired and how it resolved (e.g. a saveBase64Data permission regression).",
|
|
336
|
+
inputSchema: {
|
|
337
|
+
type: "object",
|
|
338
|
+
properties: {},
|
|
339
|
+
required: []
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "AIT.getMockState",
|
|
344
|
+
description: "Returns the devtools mock state snapshot (window.__ait) — environment, permissions, location, auth, network, IAP, and more. Read-only. In dev mode this is the live browser mock state; in debug mode the in-app side reports it over the AIT domain.",
|
|
345
|
+
inputSchema: {
|
|
346
|
+
type: "object",
|
|
347
|
+
properties: {},
|
|
348
|
+
required: []
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: "AIT.getOperationalEnvironment",
|
|
353
|
+
description: "Returns getOperationalEnvironment() plus the resolved SDK version — metadata raw CDP cannot observe. Read-only.",
|
|
354
|
+
inputSchema: {
|
|
355
|
+
type: "object",
|
|
356
|
+
properties: {},
|
|
357
|
+
required: []
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
];
|
|
361
|
+
const DEBUG_TOOL_NAMES = new Set(DEBUG_TOOL_DEFINITIONS.map((t) => t.name));
|
|
362
|
+
function isDebugToolName(name) {
|
|
363
|
+
return DEBUG_TOOL_NAMES.has(name);
|
|
364
|
+
}
|
|
365
|
+
/** Renders a CDP `RemoteObject` console arg to a stable display string. */
|
|
366
|
+
function renderRemoteObject(arg) {
|
|
367
|
+
if (arg.value !== void 0) {
|
|
368
|
+
if (typeof arg.value === "string") return arg.value;
|
|
369
|
+
try {
|
|
370
|
+
return JSON.stringify(arg.value);
|
|
371
|
+
} catch {
|
|
372
|
+
return String(arg.value);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (arg.description !== void 0) return arg.description;
|
|
376
|
+
if (arg.className !== void 0) return arg.className;
|
|
377
|
+
return arg.subtype ?? arg.type;
|
|
378
|
+
}
|
|
379
|
+
function normalizeConsoleMessage(event) {
|
|
380
|
+
const args = event.args.map(renderRemoteObject);
|
|
381
|
+
return {
|
|
382
|
+
level: event.type,
|
|
383
|
+
text: args.join(" "),
|
|
384
|
+
timestamp: event.timestamp,
|
|
385
|
+
args
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function listConsoleMessages(connection) {
|
|
389
|
+
return connection.getBufferedEvents("Runtime.consoleAPICalled").map((event) => normalizeConsoleMessage(event));
|
|
390
|
+
}
|
|
391
|
+
function listNetworkRequests(connection) {
|
|
392
|
+
const requests = connection.getBufferedEvents("Network.requestWillBeSent");
|
|
393
|
+
const responses = connection.getBufferedEvents("Network.responseReceived");
|
|
394
|
+
const responseByRequestId = /* @__PURE__ */ new Map();
|
|
395
|
+
for (const response of responses) responseByRequestId.set(response.requestId, response);
|
|
396
|
+
return requests.map((request) => {
|
|
397
|
+
const response = responseByRequestId.get(request.requestId);
|
|
398
|
+
return {
|
|
399
|
+
requestId: request.requestId,
|
|
400
|
+
url: request.request.url,
|
|
401
|
+
method: request.request.method,
|
|
402
|
+
status: response ? response.response.status : null,
|
|
403
|
+
statusText: response ? response.response.statusText : null,
|
|
404
|
+
startTime: request.timestamp,
|
|
405
|
+
endTime: response ? response.timestamp : null
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
function listPages(connection, tunnel) {
|
|
410
|
+
return {
|
|
411
|
+
pages: connection.listTargets(),
|
|
412
|
+
tunnel
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
/** Returns the DOM tree of the attached page (`DOM.getDocument`). */
|
|
416
|
+
function getDomDocument(connection) {
|
|
417
|
+
return connection.send("DOM.getDocument", {
|
|
418
|
+
depth: -1,
|
|
419
|
+
pierce: true
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
/** Returns a serialized page snapshot (`DOMSnapshot.captureSnapshot`). */
|
|
423
|
+
function takeSnapshot(connection) {
|
|
424
|
+
return connection.send("DOMSnapshot.captureSnapshot", {});
|
|
425
|
+
}
|
|
426
|
+
/** Captures a PNG screenshot of the attached page (`Page.captureScreenshot`). */
|
|
427
|
+
async function takeScreenshot(connection) {
|
|
428
|
+
const { data } = await connection.send("Page.captureScreenshot", { format: "png" });
|
|
429
|
+
return {
|
|
430
|
+
data,
|
|
431
|
+
dataUri: `data:image/png;base64,${data}`,
|
|
432
|
+
mimeType: "image/png"
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
/** Set of tool names served by the AIT source rather than the CDP connection. */
|
|
436
|
+
const AIT_TOOL_NAMES = new Set([
|
|
437
|
+
"AIT.getSdkCallHistory",
|
|
438
|
+
"AIT.getMockState",
|
|
439
|
+
"AIT.getOperationalEnvironment"
|
|
440
|
+
]);
|
|
441
|
+
/** True for the Phase 3 AIT.* tools (served by an `AitSource`, not CDP). */
|
|
442
|
+
function isAitToolName(name) {
|
|
443
|
+
return AIT_TOOL_NAMES.has(name);
|
|
444
|
+
}
|
|
445
|
+
/** Returns the recent SDK call trace (`AIT.getSdkCallHistory`). */
|
|
446
|
+
function getSdkCallHistory(source) {
|
|
447
|
+
return source.get("AIT.getSdkCallHistory");
|
|
448
|
+
}
|
|
449
|
+
/** Returns the devtools mock-state snapshot (`AIT.getMockState`). */
|
|
450
|
+
function getMockState(source) {
|
|
451
|
+
return source.get("AIT.getMockState");
|
|
452
|
+
}
|
|
453
|
+
/** Returns the operational environment + SDK version (`AIT.getOperationalEnvironment`). */
|
|
454
|
+
function getOperationalEnvironment(source) {
|
|
455
|
+
return source.get("AIT.getOperationalEnvironment");
|
|
456
|
+
}
|
|
457
|
+
//#endregion
|
|
458
|
+
//#region src/mcp/tunnel.ts
|
|
459
|
+
/**
|
|
460
|
+
* cloudflared quick tunnel + attach banner for the debug-mode MCP server.
|
|
461
|
+
*
|
|
462
|
+
* On spawn, the debug server opens an accountless `*.trycloudflare.com` quick
|
|
463
|
+
* tunnel to the local Chii relay so the phone can attach over a public wss URL,
|
|
464
|
+
* then prints that URL + a secret token + an ASCII QR to the terminal. The
|
|
465
|
+
* phone scans the QR (or pastes the URL) to attach; the in-app side passes the
|
|
466
|
+
* token back. Phase 1 only generates + displays the token and makes it
|
|
467
|
+
* available — full ACL enforcement is a later phase.
|
|
468
|
+
*
|
|
469
|
+
* Node-only: spawns the cloudflared binary and writes to stdout/stderr.
|
|
470
|
+
*/
|
|
471
|
+
/** Generates a 32-byte hex secret token used to gate attach. */
|
|
472
|
+
function generateAttachToken() {
|
|
473
|
+
return randomBytes(32).toString("hex");
|
|
474
|
+
}
|
|
475
|
+
/** Ensures the cloudflared binary is installed (downloads + caches on first run). */
|
|
476
|
+
async function ensureCloudflaredBin() {
|
|
477
|
+
const { existsSync } = await import("node:fs");
|
|
478
|
+
if (!existsSync(bin)) await install(bin);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Opens a cloudflared quick tunnel to the local relay port and resolves once
|
|
482
|
+
* the public URL is assigned.
|
|
483
|
+
*/
|
|
484
|
+
async function startQuickTunnel(localPort) {
|
|
485
|
+
await ensureCloudflaredBin();
|
|
486
|
+
const tunnel = Tunnel.quick(`http://127.0.0.1:${localPort}`);
|
|
487
|
+
const url = await new Promise((resolve, reject) => {
|
|
488
|
+
const onUrl = (assigned) => {
|
|
489
|
+
cleanup();
|
|
490
|
+
resolve(assigned);
|
|
491
|
+
};
|
|
492
|
+
const onError = (err) => {
|
|
493
|
+
cleanup();
|
|
494
|
+
reject(err);
|
|
495
|
+
};
|
|
496
|
+
const onExit = (code) => {
|
|
497
|
+
cleanup();
|
|
498
|
+
reject(/* @__PURE__ */ new Error(`cloudflared exited before assigning a URL (code ${code})`));
|
|
499
|
+
};
|
|
500
|
+
const cleanup = () => {
|
|
501
|
+
tunnel.off("url", onUrl);
|
|
502
|
+
tunnel.off("error", onError);
|
|
503
|
+
tunnel.off("exit", onExit);
|
|
504
|
+
};
|
|
505
|
+
tunnel.once("url", onUrl);
|
|
506
|
+
tunnel.once("error", onError);
|
|
507
|
+
tunnel.once("exit", onExit);
|
|
508
|
+
});
|
|
509
|
+
return {
|
|
510
|
+
url,
|
|
511
|
+
wssUrl: url.replace(/^https/, "wss"),
|
|
512
|
+
stop: () => {
|
|
513
|
+
tunnel.stop();
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
/** Renders the attach banner (URL + token + ASCII QR) as a string. */
|
|
518
|
+
async function renderAttachBanner(input) {
|
|
519
|
+
const payload = `${input.wssUrl}?token=${input.token}`;
|
|
520
|
+
const qr = await new Promise((resolve) => {
|
|
521
|
+
qrcode.generate(payload, { small: true }, (rendered) => resolve(rendered));
|
|
522
|
+
});
|
|
523
|
+
return [
|
|
524
|
+
"",
|
|
525
|
+
"AIT debug — attach a mini-app to this session",
|
|
526
|
+
"",
|
|
527
|
+
` relay (wss): ${input.wssUrl}`,
|
|
528
|
+
` token: ${input.token}`,
|
|
529
|
+
"",
|
|
530
|
+
" Open the dogfood mini-app with ?debug=1, then scan the QR",
|
|
531
|
+
" (or paste the relay URL + token in the in-app attach form):",
|
|
532
|
+
"",
|
|
533
|
+
qr
|
|
534
|
+
].join("\n");
|
|
535
|
+
}
|
|
536
|
+
/** Prints the attach banner to stderr (stdout is the MCP stdio channel). */
|
|
537
|
+
async function printAttachBanner(input) {
|
|
538
|
+
const banner = await renderAttachBanner(input);
|
|
539
|
+
process.stderr.write(`${banner}\n`);
|
|
540
|
+
}
|
|
541
|
+
//#endregion
|
|
542
|
+
//#region src/mcp/debug-server.ts
|
|
543
|
+
/**
|
|
544
|
+
* @ait-co/devtools debug-mode MCP server (stdio) — Phase 1–3.
|
|
545
|
+
*
|
|
546
|
+
* Lets an AI coding agent attach to a running mini-app (real Toss WebView, or a
|
|
547
|
+
* browser in dev mode) and read its console/network/DOM/screenshot over CDP plus
|
|
548
|
+
* the AIT.* domain, without a human watching a phone. Transport is CDP-via-Chii:
|
|
549
|
+
* a local Chii relay :9100 exposed through a cloudflared quick tunnel; the phone
|
|
550
|
+
* attaches over the public wss URL.
|
|
551
|
+
*
|
|
552
|
+
* AI host --stdio--> this server --CDP client WS--> Chii relay :9100
|
|
553
|
+
* ^-- target WS -- phone
|
|
554
|
+
*
|
|
555
|
+
* The tool layer reads from an injectable `CdpConnection` (CDP) and `AitSource`
|
|
556
|
+
* (AIT.*), so every tool is unit-testable with a fake (no phone). This module
|
|
557
|
+
* wires the live pieces (relay + tunnel + production connection); the phone
|
|
558
|
+
* roundtrip itself is phone-gated and deferred.
|
|
559
|
+
*
|
|
560
|
+
* Node-only.
|
|
561
|
+
*/
|
|
562
|
+
/**
|
|
563
|
+
* Builds the debug-mode MCP server around an injected CDP connection + AIT
|
|
564
|
+
* source + tunnel status getter. Pure wiring — does not start a relay or
|
|
565
|
+
* tunnel, which is what makes the tool surface unit-testable.
|
|
566
|
+
*/
|
|
567
|
+
function createDebugServer(deps) {
|
|
568
|
+
const { connection, aitSource, getTunnelStatus } = deps;
|
|
569
|
+
const server = new Server({
|
|
570
|
+
name: "ait-debug",
|
|
571
|
+
version: "0.1.23"
|
|
572
|
+
}, { capabilities: { tools: {} } });
|
|
573
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEBUG_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
574
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
575
|
+
const name = request.params.name;
|
|
576
|
+
if (!isDebugToolName(name)) return {
|
|
577
|
+
content: [{
|
|
578
|
+
type: "text",
|
|
579
|
+
text: `Unknown tool: ${name}`
|
|
580
|
+
}],
|
|
581
|
+
isError: true
|
|
582
|
+
};
|
|
583
|
+
if (isAitToolName(name)) try {
|
|
584
|
+
await connection.enableDomains();
|
|
585
|
+
switch (name) {
|
|
586
|
+
case "AIT.getSdkCallHistory": return jsonResult$1(await getSdkCallHistory(aitSource));
|
|
587
|
+
case "AIT.getMockState": return jsonResult$1(await getMockState(aitSource));
|
|
588
|
+
case "AIT.getOperationalEnvironment": return jsonResult$1(await getOperationalEnvironment(aitSource));
|
|
589
|
+
default: return unknownTool(name);
|
|
590
|
+
}
|
|
591
|
+
} catch (err) {
|
|
592
|
+
return errorResult(err, name);
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
await connection.enableDomains();
|
|
596
|
+
} catch (err) {
|
|
597
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
598
|
+
if (name === "list_pages") return jsonResult$1(listPages(connection, getTunnelStatus()));
|
|
599
|
+
return {
|
|
600
|
+
content: [{
|
|
601
|
+
type: "text",
|
|
602
|
+
text: `${message}\nCall list_pages to confirm a mini-app has attached over the relay.`
|
|
603
|
+
}],
|
|
604
|
+
isError: true
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
switch (name) {
|
|
609
|
+
case "list_console_messages": return jsonResult$1(listConsoleMessages(connection));
|
|
610
|
+
case "list_network_requests": return jsonResult$1(listNetworkRequests(connection));
|
|
611
|
+
case "list_pages": return jsonResult$1(listPages(connection, getTunnelStatus()));
|
|
612
|
+
case "get_dom_document": return jsonResult$1(await getDomDocument(connection));
|
|
613
|
+
case "take_snapshot": return jsonResult$1(await takeSnapshot(connection));
|
|
614
|
+
case "take_screenshot": {
|
|
615
|
+
const shot = await takeScreenshot(connection);
|
|
616
|
+
return { content: [{
|
|
617
|
+
type: "image",
|
|
618
|
+
data: shot.data,
|
|
619
|
+
mimeType: shot.mimeType
|
|
620
|
+
}] };
|
|
621
|
+
}
|
|
622
|
+
default: return unknownTool(name);
|
|
623
|
+
}
|
|
624
|
+
} catch (err) {
|
|
625
|
+
return errorResult(err, name);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
return server;
|
|
629
|
+
}
|
|
630
|
+
function jsonResult$1(value) {
|
|
631
|
+
return { content: [{
|
|
632
|
+
type: "text",
|
|
633
|
+
text: JSON.stringify(value, null, 2)
|
|
634
|
+
}] };
|
|
635
|
+
}
|
|
636
|
+
function unknownTool(name) {
|
|
637
|
+
return {
|
|
638
|
+
content: [{
|
|
639
|
+
type: "text",
|
|
640
|
+
text: `Unknown tool: ${name}`
|
|
641
|
+
}],
|
|
642
|
+
isError: true
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
function errorResult(err, name) {
|
|
646
|
+
return {
|
|
647
|
+
content: [{
|
|
648
|
+
type: "text",
|
|
649
|
+
text: `${name} failed: ${err instanceof Error ? err.message : String(err)}\nCall list_pages to confirm a mini-app has attached over the relay.`
|
|
650
|
+
}],
|
|
651
|
+
isError: true
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Boots the live debug stack and serves it over stdio:
|
|
656
|
+
* 1. start the Chii relay,
|
|
657
|
+
* 2. open a cloudflared quick tunnel to it,
|
|
658
|
+
* 3. print QR + secret token,
|
|
659
|
+
* 4. expose the debug tools backed by a `ChiiCdpConnection` + `ChiiAitSource`.
|
|
660
|
+
*/
|
|
661
|
+
async function runDebugServer(options = {}) {
|
|
662
|
+
const relayPort = options.relayPort ?? 9100;
|
|
663
|
+
const relay = await startChiiRelay({ port: relayPort });
|
|
664
|
+
let tunnel = null;
|
|
665
|
+
let tunnelStatus = {
|
|
666
|
+
up: false,
|
|
667
|
+
wssUrl: null
|
|
668
|
+
};
|
|
669
|
+
const token = generateAttachToken();
|
|
670
|
+
try {
|
|
671
|
+
tunnel = await startQuickTunnel(relayPort);
|
|
672
|
+
tunnelStatus = {
|
|
673
|
+
up: true,
|
|
674
|
+
wssUrl: tunnel.wssUrl
|
|
675
|
+
};
|
|
676
|
+
await printAttachBanner({
|
|
677
|
+
wssUrl: tunnel.wssUrl,
|
|
678
|
+
token
|
|
679
|
+
});
|
|
680
|
+
} catch (err) {
|
|
681
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
682
|
+
process.stderr.write(`[ait-debug] Failed to open cloudflared quick tunnel: ${message}\n[ait-debug] The relay is up locally; attach over the public URL is unavailable until the tunnel starts.
|
|
683
|
+
`);
|
|
684
|
+
}
|
|
685
|
+
const connection = new ChiiCdpConnection({ relayBaseUrl: relay.baseUrl });
|
|
686
|
+
const server = createDebugServer({
|
|
687
|
+
connection,
|
|
688
|
+
aitSource: new ChiiAitSource(connection),
|
|
689
|
+
getTunnelStatus: () => tunnelStatus
|
|
690
|
+
});
|
|
691
|
+
const transport = new StdioServerTransport();
|
|
692
|
+
const shutdown = () => {
|
|
693
|
+
connection.close();
|
|
694
|
+
tunnel?.stop();
|
|
695
|
+
relay.close();
|
|
696
|
+
server.close();
|
|
697
|
+
};
|
|
698
|
+
process.once("SIGINT", shutdown);
|
|
699
|
+
process.once("SIGTERM", shutdown);
|
|
700
|
+
await server.connect(transport);
|
|
701
|
+
}
|
|
702
|
+
//#endregion
|
|
703
|
+
//#region src/mcp/ait-http-source.ts
|
|
704
|
+
function isObject(value) {
|
|
705
|
+
return typeof value === "object" && value !== null;
|
|
706
|
+
}
|
|
707
|
+
var HttpAitSource = class {
|
|
708
|
+
stateEndpoint;
|
|
709
|
+
fetchImpl;
|
|
710
|
+
constructor(options) {
|
|
711
|
+
this.stateEndpoint = options.stateEndpoint;
|
|
712
|
+
this.fetchImpl = options.fetchImpl ?? ((url) => fetch(url));
|
|
713
|
+
}
|
|
714
|
+
async fetchState() {
|
|
715
|
+
const res = await this.fetchImpl(this.stateEndpoint);
|
|
716
|
+
if (!res.ok) throw new Error(`Failed to fetch mock state from ${this.stateEndpoint}: HTTP ${res.status} ${res.statusText}. Ensure the Vite dev server is running with the @ait-co/devtools unplugin option \`mcp: true\`.`);
|
|
717
|
+
const body = await res.json();
|
|
718
|
+
return isObject(body) ? body : {};
|
|
719
|
+
}
|
|
720
|
+
async get(method) {
|
|
721
|
+
switch (method) {
|
|
722
|
+
case "AIT.getMockState": return await this.fetchState();
|
|
723
|
+
case "AIT.getOperationalEnvironment": {
|
|
724
|
+
const state = await this.fetchState();
|
|
725
|
+
return {
|
|
726
|
+
environment: typeof state.environment === "string" ? state.environment : "unknown",
|
|
727
|
+
sdkVersion: typeof state.appVersion === "string" ? state.appVersion : null
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
case "AIT.getSdkCallHistory": return { calls: [] };
|
|
731
|
+
default: throw new Error(`Unknown AIT method: ${String(method)}`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
//#endregion
|
|
736
|
+
//#region src/mcp/server.ts
|
|
737
|
+
/**
|
|
738
|
+
* @ait-co/devtools dev-mode MCP server (stdio).
|
|
739
|
+
*
|
|
740
|
+
* Exposes the live browser mock state from a running Vite dev server to AI
|
|
741
|
+
* coding agents via the Model Context Protocol (MCP).
|
|
742
|
+
*
|
|
743
|
+
* Architecture:
|
|
744
|
+
* Browser (aitState) → Vite dev server endpoint (/api/ait-devtools/state)
|
|
745
|
+
* ← HTTP GET ← this stdio MCP server ← AI agent
|
|
746
|
+
*
|
|
747
|
+
* The Vite endpoint is registered by the unplugin when `mcp: true` is set in
|
|
748
|
+
* the plugin options (see `src/unplugin/index.ts`).
|
|
749
|
+
*
|
|
750
|
+
* Phase 3 tool-surface alignment: dev mode and debug mode now expose the same
|
|
751
|
+
* `AIT.*` tools (`AIT.getMockState`, `AIT.getOperationalEnvironment`,
|
|
752
|
+
* `AIT.getSdkCallHistory`). In dev mode they are backed by the HTTP mock-state
|
|
753
|
+
* endpoint (see `HttpAitSource`); in debug mode by the Chii channel. So an AI
|
|
754
|
+
* sees a coherent tool whether attached to a phone (debug) or a dev browser
|
|
755
|
+
* (dev). `devtools_get_mock_state` (the original devtools#130 name) is kept as a
|
|
756
|
+
* backward-compatible alias of `AIT.getMockState`.
|
|
757
|
+
*
|
|
758
|
+
* This module is reached via the `devtools-mcp --mode=dev` CLI entry (see
|
|
759
|
+
* `cli.ts`); the default (no flag) bin mode is the debug-mode CDP/Chii server.
|
|
760
|
+
*
|
|
761
|
+
* Usage (in your MCP client config, e.g. Claude Desktop):
|
|
762
|
+
* {
|
|
763
|
+
* "mcpServers": {
|
|
764
|
+
* "ait-devtools": {
|
|
765
|
+
* "command": "pnpm",
|
|
766
|
+
* "args": ["exec", "devtools-mcp", "--mode=dev"],
|
|
767
|
+
* "env": { "AIT_DEVTOOLS_URL": "http://localhost:5173" }
|
|
768
|
+
* }
|
|
769
|
+
* }
|
|
770
|
+
* }
|
|
771
|
+
*/
|
|
772
|
+
/** Tool descriptors served by the dev-mode server. */
|
|
773
|
+
const DEV_TOOL_DEFINITIONS = [
|
|
774
|
+
{
|
|
775
|
+
name: "AIT.getMockState",
|
|
776
|
+
description: "Returns the devtools mock state snapshot (window.__ait) from the running browser session — environment, permissions, location, auth, network, IAP, and more. Read-only. Requires the Vite dev server running with the @ait-co/devtools unplugin option `mcp: true`. Same tool as in debug mode, where the in-app side reports it over the AIT domain.",
|
|
777
|
+
inputSchema: {
|
|
778
|
+
type: "object",
|
|
779
|
+
properties: {},
|
|
780
|
+
required: []
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
name: "AIT.getOperationalEnvironment",
|
|
785
|
+
description: "Returns the operational environment + SDK/app version derived from the dev mock state. Read-only.",
|
|
786
|
+
inputSchema: {
|
|
787
|
+
type: "object",
|
|
788
|
+
properties: {},
|
|
789
|
+
required: []
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
name: "AIT.getSdkCallHistory",
|
|
794
|
+
description: "Returns the SDK call trace. In dev mode the HTTP mock-state endpoint records no trace, so this returns an empty list; in debug mode it is populated over the AIT domain. Read-only.",
|
|
795
|
+
inputSchema: {
|
|
796
|
+
type: "object",
|
|
797
|
+
properties: {},
|
|
798
|
+
required: []
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
name: "devtools_get_mock_state",
|
|
803
|
+
description: "Backward-compatible alias of AIT.getMockState (the original devtools#130 name). Returns the current AIT DevTools mock state snapshot. Read-only. Prefer AIT.getMockState in new configs.",
|
|
804
|
+
inputSchema: {
|
|
805
|
+
type: "object",
|
|
806
|
+
properties: {},
|
|
807
|
+
required: []
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
];
|
|
811
|
+
const DEV_TOOL_NAMES = new Set(DEV_TOOL_DEFINITIONS.map((t) => t.name));
|
|
812
|
+
/** Builds the dev-mode MCP server (does not connect a transport). */
|
|
813
|
+
function createDevServer(deps = {}) {
|
|
814
|
+
const stateEndpoint = `${process.env.AIT_DEVTOOLS_URL ?? "http://localhost:5173"}/api/ait-devtools/state`;
|
|
815
|
+
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
816
|
+
const server = new Server({
|
|
817
|
+
name: "ait-devtools",
|
|
818
|
+
version: "0.1.23"
|
|
819
|
+
}, { capabilities: { tools: {} } });
|
|
820
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
821
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
822
|
+
const name = request.params.name;
|
|
823
|
+
if (!DEV_TOOL_NAMES.has(name)) return {
|
|
824
|
+
content: [{
|
|
825
|
+
type: "text",
|
|
826
|
+
text: `Unknown tool: ${name}`
|
|
827
|
+
}],
|
|
828
|
+
isError: true
|
|
829
|
+
};
|
|
830
|
+
try {
|
|
831
|
+
const effective = name === "devtools_get_mock_state" ? "AIT.getMockState" : name;
|
|
832
|
+
if (!isAitToolName(effective)) return {
|
|
833
|
+
content: [{
|
|
834
|
+
type: "text",
|
|
835
|
+
text: `Unknown tool: ${name}`
|
|
836
|
+
}],
|
|
837
|
+
isError: true
|
|
838
|
+
};
|
|
839
|
+
switch (effective) {
|
|
840
|
+
case "AIT.getMockState": return jsonResult(await getMockState(aitSource));
|
|
841
|
+
case "AIT.getOperationalEnvironment": return jsonResult(await getOperationalEnvironment(aitSource));
|
|
842
|
+
case "AIT.getSdkCallHistory": return jsonResult(await getSdkCallHistory(aitSource));
|
|
843
|
+
default: return {
|
|
844
|
+
content: [{
|
|
845
|
+
type: "text",
|
|
846
|
+
text: `Unknown tool: ${name}`
|
|
847
|
+
}],
|
|
848
|
+
isError: true
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
} catch (err) {
|
|
852
|
+
return {
|
|
853
|
+
content: [{
|
|
854
|
+
type: "text",
|
|
855
|
+
text: `${err instanceof Error ? err.message : String(err)}\nIs the Vite dev server running with the @ait-co/devtools unplugin option \`mcp: true\`? Is AIT_DEVTOOLS_URL set correctly?`
|
|
856
|
+
}],
|
|
857
|
+
isError: true
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
return server;
|
|
862
|
+
}
|
|
863
|
+
function jsonResult(value) {
|
|
864
|
+
return { content: [{
|
|
865
|
+
type: "text",
|
|
866
|
+
text: JSON.stringify(value, null, 2)
|
|
867
|
+
}] };
|
|
868
|
+
}
|
|
869
|
+
/** Builds the dev-mode server and connects it over stdio. */
|
|
870
|
+
async function runDevServer() {
|
|
871
|
+
const server = createDevServer();
|
|
872
|
+
const transport = new StdioServerTransport();
|
|
873
|
+
await server.connect(transport);
|
|
874
|
+
}
|
|
875
|
+
//#endregion
|
|
876
|
+
//#region src/mcp/cli.ts
|
|
877
|
+
/**
|
|
878
|
+
* `devtools-mcp` bin entry.
|
|
879
|
+
*
|
|
880
|
+
* Single bin, two transports selected by `--mode`:
|
|
881
|
+
* - (default, no flag) debug mode — CDP/Chii relay + cloudflared quick tunnel.
|
|
882
|
+
* Attach a running mini-app (real Toss WebView or a browser) and read its
|
|
883
|
+
* console + network over CDP without a human watching a phone.
|
|
884
|
+
* - `--mode=dev` — dev mode — reads the live browser mock state from a running
|
|
885
|
+
* Vite dev server (the devtools#130 `devtools_get_mock_state` surface).
|
|
886
|
+
*
|
|
887
|
+
* Node-only stdio process.
|
|
888
|
+
*/
|
|
889
|
+
/** Parses `--mode=<value>` / `--mode <value>` from argv; default `debug`. */
|
|
890
|
+
function parseMode(argv) {
|
|
891
|
+
for (let i = 0; i < argv.length; i++) {
|
|
892
|
+
const arg = argv[i];
|
|
893
|
+
if (arg === void 0) continue;
|
|
894
|
+
if (arg.startsWith("--mode=")) return normalizeMode(arg.slice(7));
|
|
895
|
+
if (arg === "--mode") {
|
|
896
|
+
const next = argv[i + 1];
|
|
897
|
+
if (next === void 0) throw new Error("--mode requires a value: 'debug' (default) or 'dev'.");
|
|
898
|
+
return normalizeMode(next);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return "debug";
|
|
902
|
+
}
|
|
903
|
+
function normalizeMode(value) {
|
|
904
|
+
if (value === "dev") return "dev";
|
|
905
|
+
if (value === "debug") return "debug";
|
|
906
|
+
throw new Error(`Unknown --mode '${value}'. Expected 'debug' (default) or 'dev'.`);
|
|
907
|
+
}
|
|
908
|
+
async function main() {
|
|
909
|
+
if (parseMode(process.argv.slice(2)) === "dev") await runDevServer();
|
|
910
|
+
else await runDebugServer();
|
|
911
|
+
}
|
|
912
|
+
/** True when this file is the process entry (the bin), not an import. */
|
|
913
|
+
function isEntrypoint() {
|
|
914
|
+
const entry = argv[1];
|
|
915
|
+
if (entry === void 0) return false;
|
|
916
|
+
try {
|
|
917
|
+
return fileURLToPath(import.meta.url) === entry;
|
|
918
|
+
} catch {
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (isEntrypoint()) main().catch((err) => {
|
|
923
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
924
|
+
process.stderr.write(`[devtools-mcp] fatal: ${message}\n`);
|
|
925
|
+
process.exitCode = 1;
|
|
926
|
+
});
|
|
927
|
+
//#endregion
|
|
928
|
+
export { parseMode };
|
|
929
|
+
|
|
930
|
+
//# sourceMappingURL=cli.js.map
|