@mseep/anything-analyzer 3.6.50
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/.codeartsdoer/.codebaseignore +0 -0
- package/.codeartsdoer/AGENTS.md +12 -0
- package/.github/workflows/build.yml +146 -0
- package/README.en.md +264 -0
- package/README.md +276 -0
- package/RELEASE_NOTES.md +16 -0
- package/USAGE.md +490 -0
- package/color-preview-r3.html +414 -0
- package/color-preview.html +414 -0
- package/dev-app-update.yml +3 -0
- package/electron-builder.yml +36 -0
- package/electron.vite.config.ts +40 -0
- package/package.json +53 -0
- package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
- package/resources/doloffer-logo.png +0 -0
- package/resources/entitlements.mac.plist +12 -0
- package/resources/icon.ico +0 -0
- package/resources/icon.png +0 -0
- package/src/main/ai/ai-analyzer.ts +517 -0
- package/src/main/ai/crypto-script-extractor.ts +206 -0
- package/src/main/ai/data-assembler.ts +205 -0
- package/src/main/ai/llm-router.ts +1120 -0
- package/src/main/ai/prompt-builder.ts +349 -0
- package/src/main/ai/scene-detector.ts +302 -0
- package/src/main/capture/capture-engine.ts +130 -0
- package/src/main/capture/interaction-recorder.ts +171 -0
- package/src/main/capture/js-injector.ts +57 -0
- package/src/main/capture/replay-engine.ts +256 -0
- package/src/main/capture/storage-collector.ts +76 -0
- package/src/main/cdp/cdp-manager.ts +233 -0
- package/src/main/db/database.ts +41 -0
- package/src/main/db/migrations.ts +235 -0
- package/src/main/db/repositories.ts +574 -0
- package/src/main/fingerprint/http-spoofing.ts +48 -0
- package/src/main/fingerprint/presets.ts +173 -0
- package/src/main/fingerprint/profile-generator.ts +115 -0
- package/src/main/fingerprint/profile-store.ts +52 -0
- package/src/main/index.ts +260 -0
- package/src/main/ipc.ts +856 -0
- package/src/main/logger.ts +42 -0
- package/src/main/mcp/mcp-config.ts +66 -0
- package/src/main/mcp/mcp-manager.ts +155 -0
- package/src/main/mcp/mcp-server.ts +1038 -0
- package/src/main/prompt-templates.ts +170 -0
- package/src/main/proxy/ca-manager.ts +204 -0
- package/src/main/proxy/cert-download-page.ts +171 -0
- package/src/main/proxy/cert-installer.ts +242 -0
- package/src/main/proxy/mitm-proxy-config.ts +37 -0
- package/src/main/proxy/mitm-proxy-server.ts +1085 -0
- package/src/main/proxy/system-proxy.ts +248 -0
- package/src/main/session/session-manager.ts +724 -0
- package/src/main/tab-manager.ts +582 -0
- package/src/main/updater.ts +111 -0
- package/src/main/window.ts +235 -0
- package/src/preload/hook-script.ts +270 -0
- package/src/preload/index.ts +211 -0
- package/src/preload/interaction-hook.ts +286 -0
- package/src/preload/stealth-script.ts +302 -0
- package/src/preload/target-preload.ts +15 -0
- package/src/renderer/App.tsx +656 -0
- package/src/renderer/components/AiLogDetail.tsx +173 -0
- package/src/renderer/components/AiLogList.tsx +101 -0
- package/src/renderer/components/AiLogView.module.css +364 -0
- package/src/renderer/components/AiLogView.tsx +86 -0
- package/src/renderer/components/AnalyzeBar.module.css +79 -0
- package/src/renderer/components/AnalyzeBar.tsx +104 -0
- package/src/renderer/components/BrowserPanel.module.css +67 -0
- package/src/renderer/components/BrowserPanel.tsx +90 -0
- package/src/renderer/components/ControlBar.module.css +47 -0
- package/src/renderer/components/ControlBar.tsx +205 -0
- package/src/renderer/components/HookLog.tsx +132 -0
- package/src/renderer/components/InteractionLog.tsx +183 -0
- package/src/renderer/components/MCPServerModal.tsx +427 -0
- package/src/renderer/components/PromptTemplateModal.tsx +254 -0
- package/src/renderer/components/ReportView.module.css +413 -0
- package/src/renderer/components/ReportView.tsx +429 -0
- package/src/renderer/components/RequestDetail.module.css +191 -0
- package/src/renderer/components/RequestDetail.tsx +202 -0
- package/src/renderer/components/RequestLog.module.css +69 -0
- package/src/renderer/components/RequestLog.tsx +208 -0
- package/src/renderer/components/SessionList.module.css +245 -0
- package/src/renderer/components/SessionList.tsx +247 -0
- package/src/renderer/components/SettingsModal.tsx +100 -0
- package/src/renderer/components/StatusBar.module.css +44 -0
- package/src/renderer/components/StatusBar.tsx +102 -0
- package/src/renderer/components/StorageView.module.css +41 -0
- package/src/renderer/components/StorageView.tsx +178 -0
- package/src/renderer/components/TabBar.module.css +88 -0
- package/src/renderer/components/TabBar.tsx +70 -0
- package/src/renderer/components/Titlebar.module.css +254 -0
- package/src/renderer/components/Titlebar.tsx +169 -0
- package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
- package/src/renderer/components/settings/GeneralSection.tsx +164 -0
- package/src/renderer/components/settings/LLMSection.tsx +148 -0
- package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
- package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
- package/src/renderer/components/settings/ProxySection.tsx +110 -0
- package/src/renderer/css-modules.d.ts +4 -0
- package/src/renderer/hooks/useCapture.ts +383 -0
- package/src/renderer/hooks/useConfirm.tsx +91 -0
- package/src/renderer/hooks/useSession.ts +136 -0
- package/src/renderer/hooks/useTabs.ts +103 -0
- package/src/renderer/i18n/en.ts +167 -0
- package/src/renderer/i18n/index.ts +47 -0
- package/src/renderer/i18n/zh.ts +170 -0
- package/src/renderer/index.html +12 -0
- package/src/renderer/main.tsx +15 -0
- package/src/renderer/styles/global.css +144 -0
- package/src/renderer/styles/themes/ayu-dark.css +59 -0
- package/src/renderer/styles/themes/catppuccin.css +59 -0
- package/src/renderer/styles/themes/discord.css +59 -0
- package/src/renderer/styles/themes/dracula.css +59 -0
- package/src/renderer/styles/themes/github-dark.css +59 -0
- package/src/renderer/styles/themes/gruvbox.css +59 -0
- package/src/renderer/styles/themes/index.css +11 -0
- package/src/renderer/styles/themes/light.css +59 -0
- package/src/renderer/styles/themes/nord.css +59 -0
- package/src/renderer/styles/themes/one-dark.css +59 -0
- package/src/renderer/styles/themes/tokyo-night.css +59 -0
- package/src/renderer/styles/tokens.css +137 -0
- package/src/renderer/theme.ts +31 -0
- package/src/renderer/ui/Badge.module.css +38 -0
- package/src/renderer/ui/Badge.tsx +36 -0
- package/src/renderer/ui/Button.module.css +142 -0
- package/src/renderer/ui/Button.tsx +46 -0
- package/src/renderer/ui/Collapse.module.css +49 -0
- package/src/renderer/ui/Collapse.tsx +57 -0
- package/src/renderer/ui/CopyableBlock.module.css +56 -0
- package/src/renderer/ui/CopyableBlock.tsx +42 -0
- package/src/renderer/ui/Empty.module.css +19 -0
- package/src/renderer/ui/Empty.tsx +34 -0
- package/src/renderer/ui/Icons.tsx +346 -0
- package/src/renderer/ui/Input.module.css +103 -0
- package/src/renderer/ui/Input.tsx +94 -0
- package/src/renderer/ui/InputNumber.module.css +68 -0
- package/src/renderer/ui/InputNumber.tsx +104 -0
- package/src/renderer/ui/Modal.module.css +83 -0
- package/src/renderer/ui/Modal.tsx +67 -0
- package/src/renderer/ui/Popconfirm.module.css +73 -0
- package/src/renderer/ui/Popconfirm.tsx +74 -0
- package/src/renderer/ui/Progress.module.css +35 -0
- package/src/renderer/ui/Progress.tsx +30 -0
- package/src/renderer/ui/Select.module.css +91 -0
- package/src/renderer/ui/Select.tsx +100 -0
- package/src/renderer/ui/Spinner.module.css +44 -0
- package/src/renderer/ui/Spinner.tsx +27 -0
- package/src/renderer/ui/Switch.module.css +39 -0
- package/src/renderer/ui/Switch.tsx +43 -0
- package/src/renderer/ui/Tabs.module.css +76 -0
- package/src/renderer/ui/Tabs.tsx +53 -0
- package/src/renderer/ui/Tag.module.css +66 -0
- package/src/renderer/ui/Tag.tsx +47 -0
- package/src/renderer/ui/Timeline.module.css +42 -0
- package/src/renderer/ui/Timeline.tsx +29 -0
- package/src/renderer/ui/Toast.module.css +99 -0
- package/src/renderer/ui/Toast.tsx +90 -0
- package/src/renderer/ui/Tooltip.module.css +26 -0
- package/src/renderer/ui/Tooltip.tsx +23 -0
- package/src/renderer/ui/VirtualTable.module.css +230 -0
- package/src/renderer/ui/VirtualTable.tsx +416 -0
- package/src/renderer/ui/index.ts +55 -0
- package/src/shared/types.ts +695 -0
- package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
- package/tests/main/ai/llm-router.test.ts +1537 -0
- package/tests/main/ai/prompt-builder.test.ts +178 -0
- package/tests/main/ai/scene-detector.test.ts +212 -0
- package/tests/main/db/migrations.test.ts +134 -0
- package/tests/main/release-workflow.test.ts +59 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +23 -0
- package/tsconfig.web.json +24 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,1038 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import {
|
|
5
|
+
createServer,
|
|
6
|
+
type IncomingMessage,
|
|
7
|
+
type ServerResponse,
|
|
8
|
+
type Server,
|
|
9
|
+
} from "node:http";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { session } from "electron";
|
|
13
|
+
import type { SessionManager } from "../session/session-manager";
|
|
14
|
+
import type { AiAnalyzer } from "../ai/ai-analyzer";
|
|
15
|
+
import type { WindowManager } from "../window";
|
|
16
|
+
import type {
|
|
17
|
+
RequestsRepo,
|
|
18
|
+
JsHooksRepo,
|
|
19
|
+
StorageSnapshotsRepo,
|
|
20
|
+
AnalysisReportsRepo,
|
|
21
|
+
InteractionEventsRepo,
|
|
22
|
+
} from "../db/repositories";
|
|
23
|
+
import type { ChatMessage, InteractionType } from "@shared/types";
|
|
24
|
+
import { loadLLMConfig } from "../ipc";
|
|
25
|
+
import { ReplayEngine } from "../capture/replay-engine";
|
|
26
|
+
|
|
27
|
+
interface MCPServerDeps {
|
|
28
|
+
sessionManager: SessionManager;
|
|
29
|
+
aiAnalyzer: AiAnalyzer;
|
|
30
|
+
windowManager: WindowManager;
|
|
31
|
+
requestsRepo: RequestsRepo;
|
|
32
|
+
jsHooksRepo: JsHooksRepo;
|
|
33
|
+
storageSnapshotsRepo: StorageSnapshotsRepo;
|
|
34
|
+
reportsRepo: AnalysisReportsRepo;
|
|
35
|
+
interactionEventsRepo: InteractionEventsRepo;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let httpServer: Server | null = null;
|
|
39
|
+
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
40
|
+
// Per-session McpServer instances (one per transport/session)
|
|
41
|
+
const mcpServers = new Map<string, McpServer>();
|
|
42
|
+
// Per-session chat history for chat_followup tool
|
|
43
|
+
const chatHistories = new Map<string, ChatMessage[]>();
|
|
44
|
+
let currentDeps: MCPServerDeps | null = null;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if the body (single or batch JSON-RPC) contains an initialize request.
|
|
48
|
+
*/
|
|
49
|
+
function isInitRequest(body: unknown): boolean {
|
|
50
|
+
if (Array.isArray(body)) {
|
|
51
|
+
return body.some((msg) => isInitializeRequest(msg));
|
|
52
|
+
}
|
|
53
|
+
return isInitializeRequest(body);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a new McpServer instance with tools and resources registered.
|
|
58
|
+
*/
|
|
59
|
+
function createMcpServerInstance(deps: MCPServerDeps): McpServer {
|
|
60
|
+
const server = new McpServer({
|
|
61
|
+
name: "anything-analyzer",
|
|
62
|
+
version: "1.0.0",
|
|
63
|
+
});
|
|
64
|
+
registerTools(server, deps);
|
|
65
|
+
registerResources(server, deps);
|
|
66
|
+
return server;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Initialize and start the MCP Server on the given port.
|
|
71
|
+
*/
|
|
72
|
+
export async function initMCPServer(
|
|
73
|
+
deps: MCPServerDeps,
|
|
74
|
+
port: number,
|
|
75
|
+
authEnabled: boolean = true,
|
|
76
|
+
authToken: string = '',
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
if (httpServer) await stopMCPServer();
|
|
79
|
+
|
|
80
|
+
currentDeps = deps;
|
|
81
|
+
|
|
82
|
+
httpServer = createServer(
|
|
83
|
+
async (req: IncomingMessage, res: ServerResponse) => {
|
|
84
|
+
// CORS
|
|
85
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
86
|
+
res.setHeader(
|
|
87
|
+
"Access-Control-Allow-Methods",
|
|
88
|
+
"GET, POST, DELETE, OPTIONS",
|
|
89
|
+
);
|
|
90
|
+
res.setHeader(
|
|
91
|
+
"Access-Control-Allow-Headers",
|
|
92
|
+
"Content-Type, Authorization, mcp-session-id, mcp-protocol-version",
|
|
93
|
+
);
|
|
94
|
+
res.setHeader(
|
|
95
|
+
"Access-Control-Expose-Headers",
|
|
96
|
+
"mcp-session-id, mcp-protocol-version",
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (req.method === "OPTIONS") {
|
|
100
|
+
res.writeHead(204);
|
|
101
|
+
res.end();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Authentication check (skip OPTIONS preflight)
|
|
106
|
+
if (authEnabled && authToken) {
|
|
107
|
+
const authHeader = req.headers["authorization"];
|
|
108
|
+
if (authHeader !== `Bearer ${authToken}`) {
|
|
109
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
110
|
+
res.end(JSON.stringify({ error: "Unauthorized: invalid or missing token" }));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
116
|
+
if (url.pathname !== "/mcp") {
|
|
117
|
+
res.writeHead(404);
|
|
118
|
+
res.end("Not Found");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
124
|
+
|
|
125
|
+
if (req.method === "DELETE") {
|
|
126
|
+
if (sessionId && transports.has(sessionId)) {
|
|
127
|
+
// Delegate to transport so internal state is cleaned up properly
|
|
128
|
+
await transports.get(sessionId)!.handleRequest(req, res);
|
|
129
|
+
} else {
|
|
130
|
+
res.writeHead(sessionId ? 404 : 400);
|
|
131
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (req.method === "GET") {
|
|
137
|
+
if (sessionId && transports.has(sessionId)) {
|
|
138
|
+
await transports.get(sessionId)!.handleRequest(req, res);
|
|
139
|
+
} else {
|
|
140
|
+
res.writeHead(sessionId ? 404 : 400);
|
|
141
|
+
res.end(
|
|
142
|
+
JSON.stringify({ error: "Missing or invalid session ID" }),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// POST
|
|
149
|
+
const body = await readBody(req);
|
|
150
|
+
|
|
151
|
+
if (sessionId && transports.has(sessionId)) {
|
|
152
|
+
await transports.get(sessionId)!.handleRequest(req, res, body);
|
|
153
|
+
} else if (!sessionId && isInitRequest(body)) {
|
|
154
|
+
// Create per-session McpServer first so it can be captured in the callback
|
|
155
|
+
const sessionServer = createMcpServerInstance(currentDeps!);
|
|
156
|
+
const transport = new StreamableHTTPServerTransport({
|
|
157
|
+
sessionIdGenerator: () => randomUUID(),
|
|
158
|
+
onsessioninitialized: (sid) => {
|
|
159
|
+
transports.set(sid, transport);
|
|
160
|
+
// Store mcpServer here – sessionId is available now
|
|
161
|
+
mcpServers.set(sid, sessionServer);
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
transport.onclose = () => {
|
|
165
|
+
if (transport.sessionId) {
|
|
166
|
+
transports.delete(transport.sessionId);
|
|
167
|
+
const srv = mcpServers.get(transport.sessionId);
|
|
168
|
+
if (srv) {
|
|
169
|
+
srv.close().catch(() => {});
|
|
170
|
+
mcpServers.delete(transport.sessionId);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
await sessionServer.connect(transport);
|
|
175
|
+
await transport.handleRequest(req, res, body);
|
|
176
|
+
} else if (sessionId && !transports.has(sessionId)) {
|
|
177
|
+
// Session ID provided but transport is gone (e.g. server restarted)
|
|
178
|
+
res.writeHead(404);
|
|
179
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
180
|
+
} else {
|
|
181
|
+
res.writeHead(400);
|
|
182
|
+
res.end(
|
|
183
|
+
JSON.stringify({
|
|
184
|
+
error:
|
|
185
|
+
"Bad request: missing session ID or not an initialize request",
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.error("[MCP Server] Error handling request:", err);
|
|
191
|
+
if (!res.headersSent) {
|
|
192
|
+
res.writeHead(500);
|
|
193
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
httpServer.listen(port, () => {
|
|
200
|
+
console.log(`[MCP Server] Listening on http://localhost:${port}/mcp`);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Stop the MCP Server and close all connections.
|
|
206
|
+
*/
|
|
207
|
+
export async function stopMCPServer(): Promise<void> {
|
|
208
|
+
for (const transport of transports.values()) {
|
|
209
|
+
await transport.close().catch(() => {});
|
|
210
|
+
}
|
|
211
|
+
transports.clear();
|
|
212
|
+
chatHistories.clear();
|
|
213
|
+
|
|
214
|
+
for (const srv of mcpServers.values()) {
|
|
215
|
+
await srv.close().catch(() => {});
|
|
216
|
+
}
|
|
217
|
+
mcpServers.clear();
|
|
218
|
+
currentDeps = null;
|
|
219
|
+
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
if (httpServer) {
|
|
222
|
+
httpServer.close(() => {
|
|
223
|
+
httpServer = null;
|
|
224
|
+
resolve();
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
resolve();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check if MCP Server is currently running.
|
|
234
|
+
*/
|
|
235
|
+
export function isMCPServerRunning(): boolean {
|
|
236
|
+
return httpServer !== null && httpServer.listening;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---- Tool Registration ----
|
|
240
|
+
|
|
241
|
+
function registerTools(server: McpServer, deps: MCPServerDeps): void {
|
|
242
|
+
const {
|
|
243
|
+
sessionManager,
|
|
244
|
+
aiAnalyzer,
|
|
245
|
+
windowManager,
|
|
246
|
+
requestsRepo,
|
|
247
|
+
jsHooksRepo,
|
|
248
|
+
storageSnapshotsRepo,
|
|
249
|
+
reportsRepo,
|
|
250
|
+
interactionEventsRepo,
|
|
251
|
+
} = deps;
|
|
252
|
+
|
|
253
|
+
// -- Session Management --
|
|
254
|
+
|
|
255
|
+
server.registerTool(
|
|
256
|
+
"list_sessions",
|
|
257
|
+
{
|
|
258
|
+
description: "List all analysis sessions",
|
|
259
|
+
inputSchema: z.object({}),
|
|
260
|
+
},
|
|
261
|
+
async () => {
|
|
262
|
+
const sessions = sessionManager.listSessions();
|
|
263
|
+
return text(sessions);
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
server.registerTool(
|
|
268
|
+
"create_session",
|
|
269
|
+
{
|
|
270
|
+
description: "Create a new analysis session",
|
|
271
|
+
inputSchema: z.object({
|
|
272
|
+
name: z.string().describe("Session name"),
|
|
273
|
+
targetUrl: z.string().describe("Target URL to analyze"),
|
|
274
|
+
}),
|
|
275
|
+
},
|
|
276
|
+
async ({ name, targetUrl }) => {
|
|
277
|
+
const s = sessionManager.createSession(name, targetUrl);
|
|
278
|
+
return text(s);
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
server.registerTool(
|
|
283
|
+
"start_capture",
|
|
284
|
+
{
|
|
285
|
+
description:
|
|
286
|
+
"Start capturing HTTP requests for a session. The embedded browser must be open.",
|
|
287
|
+
inputSchema: z.object({
|
|
288
|
+
sessionId: z.string().describe("Session ID"),
|
|
289
|
+
}),
|
|
290
|
+
},
|
|
291
|
+
async ({ sessionId }) => {
|
|
292
|
+
const tabManager = windowManager.getTabManager();
|
|
293
|
+
const mainWin = windowManager.getMainWindow();
|
|
294
|
+
if (!tabManager || !mainWin) throw new Error("Browser not ready");
|
|
295
|
+
await sessionManager.startCapture(
|
|
296
|
+
sessionId,
|
|
297
|
+
tabManager,
|
|
298
|
+
mainWin.webContents,
|
|
299
|
+
);
|
|
300
|
+
return text({ success: true });
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
server.registerTool(
|
|
305
|
+
"pause_capture",
|
|
306
|
+
{
|
|
307
|
+
description: "Pause capturing for a session",
|
|
308
|
+
inputSchema: z.object({ sessionId: z.string() }),
|
|
309
|
+
},
|
|
310
|
+
async ({ sessionId }) => {
|
|
311
|
+
await sessionManager.pauseCapture(sessionId);
|
|
312
|
+
return text({ success: true });
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
server.registerTool(
|
|
317
|
+
"resume_capture",
|
|
318
|
+
{
|
|
319
|
+
description: "Resume capturing for a paused session",
|
|
320
|
+
inputSchema: z.object({ sessionId: z.string() }),
|
|
321
|
+
},
|
|
322
|
+
async ({ sessionId }) => {
|
|
323
|
+
await sessionManager.resumeCapture(sessionId);
|
|
324
|
+
return text({ success: true });
|
|
325
|
+
},
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
server.registerTool(
|
|
329
|
+
"stop_capture",
|
|
330
|
+
{
|
|
331
|
+
description: "Stop capturing and finalize a session",
|
|
332
|
+
inputSchema: z.object({ sessionId: z.string() }),
|
|
333
|
+
},
|
|
334
|
+
async ({ sessionId }) => {
|
|
335
|
+
await sessionManager.stopCapture(sessionId);
|
|
336
|
+
return text({ success: true });
|
|
337
|
+
},
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
server.registerTool(
|
|
341
|
+
"delete_session",
|
|
342
|
+
{
|
|
343
|
+
description: "Delete a session and all its data",
|
|
344
|
+
inputSchema: z.object({ sessionId: z.string() }),
|
|
345
|
+
},
|
|
346
|
+
async ({ sessionId }) => {
|
|
347
|
+
await sessionManager.deleteSession(sessionId);
|
|
348
|
+
return text({ success: true });
|
|
349
|
+
},
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// -- Browser Control --
|
|
353
|
+
|
|
354
|
+
server.registerTool(
|
|
355
|
+
"navigate",
|
|
356
|
+
{
|
|
357
|
+
description: "Navigate the active browser tab to a URL",
|
|
358
|
+
inputSchema: z.object({ url: z.string().describe("URL to navigate to") }),
|
|
359
|
+
},
|
|
360
|
+
async ({ url }) => {
|
|
361
|
+
await windowManager.navigateTo(url);
|
|
362
|
+
return text({ success: true, url });
|
|
363
|
+
},
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
server.registerTool(
|
|
367
|
+
"browser_back",
|
|
368
|
+
{
|
|
369
|
+
description: "Go back in the active browser tab",
|
|
370
|
+
inputSchema: z.object({}),
|
|
371
|
+
},
|
|
372
|
+
async () => {
|
|
373
|
+
windowManager.goBack();
|
|
374
|
+
return text({ success: true });
|
|
375
|
+
},
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
server.registerTool(
|
|
379
|
+
"browser_forward",
|
|
380
|
+
{
|
|
381
|
+
description: "Go forward in the active browser tab",
|
|
382
|
+
inputSchema: z.object({}),
|
|
383
|
+
},
|
|
384
|
+
async () => {
|
|
385
|
+
windowManager.goForward();
|
|
386
|
+
return text({ success: true });
|
|
387
|
+
},
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
server.registerTool(
|
|
391
|
+
"browser_reload",
|
|
392
|
+
{
|
|
393
|
+
description: "Reload the active browser tab",
|
|
394
|
+
inputSchema: z.object({}),
|
|
395
|
+
},
|
|
396
|
+
async () => {
|
|
397
|
+
windowManager.reload();
|
|
398
|
+
return text({ success: true });
|
|
399
|
+
},
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
server.registerTool(
|
|
403
|
+
"create_tab",
|
|
404
|
+
{
|
|
405
|
+
description: "Create a new browser tab",
|
|
406
|
+
inputSchema: z.object({
|
|
407
|
+
url: z.string().optional().describe("Optional URL to open"),
|
|
408
|
+
}),
|
|
409
|
+
},
|
|
410
|
+
async ({ url }) => {
|
|
411
|
+
const tabManager = windowManager.getTabManager();
|
|
412
|
+
if (!tabManager) throw new Error("Browser not ready");
|
|
413
|
+
const tab = tabManager.createTab(url);
|
|
414
|
+
return text({ id: tab.id, url: tab.url, title: tab.title });
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
server.registerTool(
|
|
419
|
+
"close_tab",
|
|
420
|
+
{
|
|
421
|
+
description: "Close a browser tab",
|
|
422
|
+
inputSchema: z.object({ tabId: z.string() }),
|
|
423
|
+
},
|
|
424
|
+
async ({ tabId }) => {
|
|
425
|
+
const tabManager = windowManager.getTabManager();
|
|
426
|
+
if (!tabManager) throw new Error("Browser not ready");
|
|
427
|
+
tabManager.closeTab(tabId);
|
|
428
|
+
return text({ success: true });
|
|
429
|
+
},
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
server.registerTool(
|
|
433
|
+
"list_tabs",
|
|
434
|
+
{
|
|
435
|
+
description: "List all browser tabs with their URLs and titles",
|
|
436
|
+
inputSchema: z.object({}),
|
|
437
|
+
},
|
|
438
|
+
async () => {
|
|
439
|
+
const tabManager = windowManager.getTabManager();
|
|
440
|
+
if (!tabManager) throw new Error("Browser not ready");
|
|
441
|
+
const tabs = tabManager.getAllTabs().map((t) => ({
|
|
442
|
+
id: t.id,
|
|
443
|
+
url: t.url,
|
|
444
|
+
title: t.title,
|
|
445
|
+
}));
|
|
446
|
+
return text(tabs);
|
|
447
|
+
},
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
server.registerTool(
|
|
451
|
+
"clear_browser_env",
|
|
452
|
+
{
|
|
453
|
+
description:
|
|
454
|
+
"Clear all browser data (cookies, localStorage, sessionStorage, cache). Current login state will be lost.",
|
|
455
|
+
inputSchema: z.object({}),
|
|
456
|
+
},
|
|
457
|
+
async () => {
|
|
458
|
+
await session.defaultSession.clearStorageData();
|
|
459
|
+
await session.defaultSession.clearCache();
|
|
460
|
+
const wc = windowManager.getTabManager()?.getActiveWebContents();
|
|
461
|
+
if (wc && !wc.isDestroyed()) wc.reload();
|
|
462
|
+
return text({ success: true });
|
|
463
|
+
},
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
server.registerTool(
|
|
467
|
+
"browser_screenshot",
|
|
468
|
+
{
|
|
469
|
+
description:
|
|
470
|
+
"Capture a screenshot of the current active browser tab. Returns a PNG image.",
|
|
471
|
+
inputSchema: z.object({}),
|
|
472
|
+
},
|
|
473
|
+
async () => {
|
|
474
|
+
const webContents = windowManager.getTabManager()?.getActiveWebContents();
|
|
475
|
+
if (!webContents || webContents.isDestroyed()) throw new Error("Browser not ready");
|
|
476
|
+
const image = await webContents.capturePage();
|
|
477
|
+
const base64 = image.toPNG().toString("base64");
|
|
478
|
+
return {
|
|
479
|
+
content: [{ type: "image" as const, data: base64, mimeType: "image/png" }],
|
|
480
|
+
};
|
|
481
|
+
},
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
server.registerTool(
|
|
485
|
+
"cdp_send_command",
|
|
486
|
+
{
|
|
487
|
+
description:
|
|
488
|
+
"Send a raw Chrome DevTools Protocol (CDP) command to the active browser tab. " +
|
|
489
|
+
"Requires an active capture session with CDP attached. " +
|
|
490
|
+
"Supports all CDP domains: Page, DOM, Runtime, Network, Emulation, Input, etc. " +
|
|
491
|
+
"See https://chromedevtools.github.io/devtools-protocol/ for available methods.",
|
|
492
|
+
inputSchema: z.object({
|
|
493
|
+
method: z.string().describe("CDP method name, e.g. 'Page.captureScreenshot', 'Runtime.evaluate', 'DOM.getDocument'"),
|
|
494
|
+
params: z.record(z.string(), z.unknown()).optional().describe("CDP method parameters as a JSON object"),
|
|
495
|
+
}),
|
|
496
|
+
},
|
|
497
|
+
async ({ method, params }) => {
|
|
498
|
+
const result = await sessionManager.sendCdpCommand(method, params as Record<string, unknown> | undefined);
|
|
499
|
+
return text(result);
|
|
500
|
+
},
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
// -- Data Query --
|
|
504
|
+
|
|
505
|
+
server.registerTool(
|
|
506
|
+
"get_requests",
|
|
507
|
+
{
|
|
508
|
+
description:
|
|
509
|
+
"Get all captured HTTP requests for a session. Returns method, url, status, headers, body, and response for each request.",
|
|
510
|
+
inputSchema: z.object({ sessionId: z.string() }),
|
|
511
|
+
},
|
|
512
|
+
async ({ sessionId }) => {
|
|
513
|
+
const requests = requestsRepo.findBySession(sessionId);
|
|
514
|
+
// Trim large bodies to keep response manageable
|
|
515
|
+
const trimmed = requests.map((r) => ({
|
|
516
|
+
id: r.id,
|
|
517
|
+
sequence: r.sequence,
|
|
518
|
+
method: r.method,
|
|
519
|
+
url: r.url,
|
|
520
|
+
status_code: r.status_code,
|
|
521
|
+
content_type: r.content_type,
|
|
522
|
+
duration_ms: r.duration_ms,
|
|
523
|
+
request_body: r.request_body
|
|
524
|
+
? r.request_body.length > 2000
|
|
525
|
+
? r.request_body.substring(0, 2000) + "..."
|
|
526
|
+
: r.request_body
|
|
527
|
+
: null,
|
|
528
|
+
response_body: r.response_body
|
|
529
|
+
? r.response_body.length > 2000
|
|
530
|
+
? r.response_body.substring(0, 2000) + "..."
|
|
531
|
+
: r.response_body
|
|
532
|
+
: null,
|
|
533
|
+
}));
|
|
534
|
+
return text(trimmed);
|
|
535
|
+
},
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
server.registerTool(
|
|
539
|
+
"filter_requests",
|
|
540
|
+
{
|
|
541
|
+
description:
|
|
542
|
+
"Filter captured HTTP requests for a session by method, domain, status code, content type, or URL pattern. Returns matching requests with trimmed bodies.",
|
|
543
|
+
inputSchema: z.object({
|
|
544
|
+
sessionId: z.string(),
|
|
545
|
+
method: z.string().optional().describe("HTTP method filter, e.g. GET, POST"),
|
|
546
|
+
domain: z.string().optional().describe("Domain/host to match in URL"),
|
|
547
|
+
statusCode: z.number().optional().describe("Exact status code, e.g. 200, 404"),
|
|
548
|
+
statusRange: z.string().optional().describe("Status code range: 2xx, 3xx, 4xx, 5xx"),
|
|
549
|
+
contentType: z.string().optional().describe("Content-Type contains match, e.g. json, html"),
|
|
550
|
+
urlPattern: z.string().optional().describe("URL substring match"),
|
|
551
|
+
limit: z.number().optional().describe("Max results to return (default 50)"),
|
|
552
|
+
}),
|
|
553
|
+
},
|
|
554
|
+
async ({ sessionId, method, domain, statusCode, statusRange, contentType, urlPattern, limit }) => {
|
|
555
|
+
const requests = requestsRepo.findBySessionFiltered(sessionId, {
|
|
556
|
+
method, domain, statusCode, statusRange, contentType, urlPattern, limit,
|
|
557
|
+
});
|
|
558
|
+
const trimmed = requests.map((r) => ({
|
|
559
|
+
id: r.id,
|
|
560
|
+
sequence: r.sequence,
|
|
561
|
+
method: r.method,
|
|
562
|
+
url: r.url,
|
|
563
|
+
status_code: r.status_code,
|
|
564
|
+
content_type: r.content_type,
|
|
565
|
+
duration_ms: r.duration_ms,
|
|
566
|
+
request_body: r.request_body
|
|
567
|
+
? r.request_body.length > 2000
|
|
568
|
+
? r.request_body.substring(0, 2000) + "..."
|
|
569
|
+
: r.request_body
|
|
570
|
+
: null,
|
|
571
|
+
response_body: r.response_body
|
|
572
|
+
? r.response_body.length > 2000
|
|
573
|
+
? r.response_body.substring(0, 2000) + "..."
|
|
574
|
+
: r.response_body
|
|
575
|
+
: null,
|
|
576
|
+
}));
|
|
577
|
+
return text(trimmed);
|
|
578
|
+
},
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
server.registerTool(
|
|
582
|
+
"get_request_detail",
|
|
583
|
+
{
|
|
584
|
+
description:
|
|
585
|
+
"Get full details of a single captured request including complete headers, body, and response body",
|
|
586
|
+
inputSchema: z.object({ requestId: z.string() }),
|
|
587
|
+
},
|
|
588
|
+
async ({ requestId }) => {
|
|
589
|
+
const req = requestsRepo.findById(requestId);
|
|
590
|
+
if (!req) return text({ error: "Request not found" });
|
|
591
|
+
return text(req);
|
|
592
|
+
},
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
server.registerTool(
|
|
596
|
+
"get_hooks",
|
|
597
|
+
{
|
|
598
|
+
description:
|
|
599
|
+
"Get all JS Hook records for a session (crypto operations, XHR/fetch intercepts, etc.)",
|
|
600
|
+
inputSchema: z.object({ sessionId: z.string() }),
|
|
601
|
+
},
|
|
602
|
+
async ({ sessionId }) => {
|
|
603
|
+
const hooks = jsHooksRepo.findBySession(sessionId);
|
|
604
|
+
return text(hooks);
|
|
605
|
+
},
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
server.registerTool(
|
|
609
|
+
"get_storage",
|
|
610
|
+
{
|
|
611
|
+
description:
|
|
612
|
+
"Get storage snapshots (cookies, localStorage, sessionStorage) for a session",
|
|
613
|
+
inputSchema: z.object({ sessionId: z.string() }),
|
|
614
|
+
},
|
|
615
|
+
async ({ sessionId }) => {
|
|
616
|
+
const snapshots = storageSnapshotsRepo.findBySession(sessionId);
|
|
617
|
+
return text(snapshots);
|
|
618
|
+
},
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
// -- AI Analysis --
|
|
622
|
+
|
|
623
|
+
server.registerTool(
|
|
624
|
+
"run_analysis",
|
|
625
|
+
{
|
|
626
|
+
description:
|
|
627
|
+
"Run AI-powered protocol analysis on captured session data. Uses the LLM configured in app settings. Returns a full analysis report.",
|
|
628
|
+
inputSchema: z.object({
|
|
629
|
+
sessionId: z.string().describe("Session ID to analyze"),
|
|
630
|
+
purpose: z
|
|
631
|
+
.string()
|
|
632
|
+
.optional()
|
|
633
|
+
.describe(
|
|
634
|
+
"Analysis focus: 'reverse-api', 'security-audit', 'performance', 'crypto-reverse', or custom text",
|
|
635
|
+
),
|
|
636
|
+
selectedSeqs: z
|
|
637
|
+
.array(z.number())
|
|
638
|
+
.optional()
|
|
639
|
+
.describe("Optional: specific request sequence numbers to analyze"),
|
|
640
|
+
}),
|
|
641
|
+
},
|
|
642
|
+
async ({ sessionId, purpose, selectedSeqs }) => {
|
|
643
|
+
const config = loadLLMConfig();
|
|
644
|
+
if (!config)
|
|
645
|
+
return text({
|
|
646
|
+
error:
|
|
647
|
+
"LLM not configured. Please configure LLM settings in the app first.",
|
|
648
|
+
});
|
|
649
|
+
const report = await aiAnalyzer.analyze(
|
|
650
|
+
sessionId,
|
|
651
|
+
config,
|
|
652
|
+
undefined,
|
|
653
|
+
purpose,
|
|
654
|
+
undefined,
|
|
655
|
+
selectedSeqs,
|
|
656
|
+
);
|
|
657
|
+
// Reset chat history so next chat_followup uses the new report
|
|
658
|
+
chatHistories.delete(sessionId);
|
|
659
|
+
return text({
|
|
660
|
+
id: report.id,
|
|
661
|
+
content: report.report_content,
|
|
662
|
+
model: report.llm_model,
|
|
663
|
+
});
|
|
664
|
+
},
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
server.registerTool(
|
|
668
|
+
"get_reports",
|
|
669
|
+
{
|
|
670
|
+
description: "Get all analysis reports for a session",
|
|
671
|
+
inputSchema: z.object({ sessionId: z.string() }),
|
|
672
|
+
},
|
|
673
|
+
async ({ sessionId }) => {
|
|
674
|
+
const reports = reportsRepo.findBySession(sessionId);
|
|
675
|
+
return text(
|
|
676
|
+
reports.map((r) => ({
|
|
677
|
+
id: r.id,
|
|
678
|
+
created_at: r.created_at,
|
|
679
|
+
llm_model: r.llm_model,
|
|
680
|
+
content:
|
|
681
|
+
r.report_content.length > 3000
|
|
682
|
+
? r.report_content.substring(0, 3000) + "..."
|
|
683
|
+
: r.report_content,
|
|
684
|
+
})),
|
|
685
|
+
);
|
|
686
|
+
},
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
server.registerTool(
|
|
690
|
+
"chat_followup",
|
|
691
|
+
{
|
|
692
|
+
description:
|
|
693
|
+
"Send a follow-up question about a previous analysis. Maintains conversation history per session.",
|
|
694
|
+
inputSchema: z.object({
|
|
695
|
+
sessionId: z.string().describe("Session ID"),
|
|
696
|
+
message: z.string().describe("Follow-up question"),
|
|
697
|
+
}),
|
|
698
|
+
},
|
|
699
|
+
async ({ sessionId, message }) => {
|
|
700
|
+
const config = loadLLMConfig();
|
|
701
|
+
if (!config) return text({ error: "LLM not configured" });
|
|
702
|
+
|
|
703
|
+
// Get or initialize chat history for this session
|
|
704
|
+
if (!chatHistories.has(sessionId)) {
|
|
705
|
+
// Load existing report as context
|
|
706
|
+
const reports = reportsRepo.findBySession(sessionId);
|
|
707
|
+
const lastReport = reports[reports.length - 1];
|
|
708
|
+
|
|
709
|
+
// Build system prompt with captured data summary (consistent with IPC path)
|
|
710
|
+
const requests = requestsRepo.findBySession(sessionId);
|
|
711
|
+
const hooks = jsHooksRepo.findBySession(sessionId);
|
|
712
|
+
const reqSummary = requests.slice(0, 50).map((r) => {
|
|
713
|
+
let path = r.url;
|
|
714
|
+
try { path = new URL(r.url).pathname; } catch { /* keep full url */ }
|
|
715
|
+
return `#${r.sequence} ${r.method} ${path} → ${r.status_code ?? '?'}`;
|
|
716
|
+
}).join('\n');
|
|
717
|
+
|
|
718
|
+
const hookSummary = hooks.length > 0
|
|
719
|
+
? '\n\nDetected hooks:\n' + hooks.slice(0, 20).map((h) =>
|
|
720
|
+
`[${h.hook_type}] ${h.function_name}`
|
|
721
|
+
).join('\n')
|
|
722
|
+
: '';
|
|
723
|
+
|
|
724
|
+
const contextBlock = reqSummary
|
|
725
|
+
? `\n\n<captured_data_summary>\nCaptured ${requests.length} requests:\n${reqSummary}${requests.length > 50 ? `\n... and ${requests.length - 50} more` : ''}${hookSummary}\n</captured_data_summary>`
|
|
726
|
+
: '';
|
|
727
|
+
|
|
728
|
+
const systemContent = `你是一位网站协议分析专家。基于之前的分析报告和捕获数据,回答用户的追问。保持技术精确,用中文回复。\n\n你可以使用 get_request_detail 工具,通过传入请求序号(seq)来查看任意请求的完整详情(请求头、请求体、响应头、响应体)。当用户追问某个具体请求或需要更多细节时,请主动调用此工具获取数据。${contextBlock}`;
|
|
729
|
+
|
|
730
|
+
const initialHistory: ChatMessage[] = [
|
|
731
|
+
{ role: "system" as const, content: systemContent },
|
|
732
|
+
];
|
|
733
|
+
if (lastReport) {
|
|
734
|
+
initialHistory.push({ role: "assistant" as const, content: lastReport.report_content });
|
|
735
|
+
}
|
|
736
|
+
chatHistories.set(sessionId, initialHistory);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const history = chatHistories.get(sessionId)!;
|
|
740
|
+
const reply = await aiAnalyzer.chat(sessionId, config, history, message);
|
|
741
|
+
// Update history
|
|
742
|
+
history.push({ role: "user" as const, content: message });
|
|
743
|
+
history.push({ role: "assistant" as const, content: reply });
|
|
744
|
+
|
|
745
|
+
return text({ reply });
|
|
746
|
+
},
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
// -- Interaction Recording --
|
|
750
|
+
|
|
751
|
+
const replayEngine = new ReplayEngine();
|
|
752
|
+
|
|
753
|
+
server.registerTool(
|
|
754
|
+
"get_interactions",
|
|
755
|
+
{
|
|
756
|
+
description:
|
|
757
|
+
"Get recorded user interaction events (clicks, inputs, scrolls, mouse movements) for a session. " +
|
|
758
|
+
"Returns element selectors, positions, input values, and timestamps.",
|
|
759
|
+
inputSchema: z.object({
|
|
760
|
+
sessionId: z.string().describe("Session ID"),
|
|
761
|
+
type: z.enum(['click', 'dblclick', 'input', 'scroll', 'navigate', 'hover']).optional().describe("Filter by interaction type"),
|
|
762
|
+
limit: z.number().default(100).describe("Max events to return"),
|
|
763
|
+
}),
|
|
764
|
+
},
|
|
765
|
+
async ({ sessionId, type, limit }) => {
|
|
766
|
+
const events = type
|
|
767
|
+
? interactionEventsRepo.findBySessionAndType(sessionId, type as InteractionType)
|
|
768
|
+
: interactionEventsRepo.findBySession(sessionId, limit);
|
|
769
|
+
return text(events.slice(0, limit));
|
|
770
|
+
},
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
server.registerTool(
|
|
774
|
+
"get_interaction_summary",
|
|
775
|
+
{
|
|
776
|
+
description:
|
|
777
|
+
"Get a high-level summary of recorded interactions: action sequence, key elements, and navigation flow. " +
|
|
778
|
+
"Useful for understanding what the user did before asking AI to automate it.",
|
|
779
|
+
inputSchema: z.object({ sessionId: z.string() }),
|
|
780
|
+
},
|
|
781
|
+
async ({ sessionId }) => {
|
|
782
|
+
const events = interactionEventsRepo.findBySession(sessionId, 500);
|
|
783
|
+
if (events.length === 0) {
|
|
784
|
+
return text({ summary: "No interactions recorded for this session.", steps: [] });
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Generate human-readable summary
|
|
788
|
+
const steps: string[] = [];
|
|
789
|
+
let stepNum = 1;
|
|
790
|
+
for (const event of events) {
|
|
791
|
+
if (event.type === 'hover') continue; // skip movement in summary
|
|
792
|
+
let desc = '';
|
|
793
|
+
switch (event.type) {
|
|
794
|
+
case 'click':
|
|
795
|
+
case 'dblclick': {
|
|
796
|
+
const target = event.element_text || event.selector || `(${event.x}, ${event.y})`;
|
|
797
|
+
desc = `${event.type === 'dblclick' ? 'Double-click' : 'Click'} "${target}" [${event.tag_name || 'element'}]`;
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
case 'input': {
|
|
801
|
+
const field = event.selector || event.tag_name || 'input';
|
|
802
|
+
desc = `Type "${event.input_value}" into ${field}`;
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
case 'scroll': {
|
|
806
|
+
desc = `Scroll to (${event.scroll_x}, ${event.scroll_y})`;
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
case 'navigate': {
|
|
810
|
+
desc = `Navigate to ${event.url}`;
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (desc) {
|
|
815
|
+
steps.push(`${stepNum++}. ${desc}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return text({
|
|
820
|
+
totalEvents: events.length,
|
|
821
|
+
clickCount: events.filter(e => e.type === 'click' || e.type === 'dblclick').length,
|
|
822
|
+
inputCount: events.filter(e => e.type === 'input').length,
|
|
823
|
+
scrollCount: events.filter(e => e.type === 'scroll').length,
|
|
824
|
+
pagesVisited: [...new Set(events.map(e => e.url))],
|
|
825
|
+
steps,
|
|
826
|
+
});
|
|
827
|
+
},
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
server.registerTool(
|
|
831
|
+
"replay_interactions",
|
|
832
|
+
{
|
|
833
|
+
description:
|
|
834
|
+
"Replay recorded user interactions in the browser via CDP Input simulation. " +
|
|
835
|
+
"Reproduces clicks, inputs, scrolls in the original sequence.",
|
|
836
|
+
inputSchema: z.object({
|
|
837
|
+
sessionId: z.string().describe("Session ID with recorded interactions"),
|
|
838
|
+
speed: z.number().default(2).describe("Playback speed multiplier (2 = 2x faster)"),
|
|
839
|
+
fromSequence: z.number().optional().describe("Start from this sequence number"),
|
|
840
|
+
toSequence: z.number().optional().describe("Stop at this sequence number"),
|
|
841
|
+
skipMoves: z.boolean().default(true).describe("Skip mouse movement events"),
|
|
842
|
+
}),
|
|
843
|
+
},
|
|
844
|
+
async ({ sessionId, speed, fromSequence, toSequence, skipMoves }) => {
|
|
845
|
+
const webContents = windowManager.getTabManager()?.getActiveWebContents();
|
|
846
|
+
if (!webContents || webContents.isDestroyed()) throw new Error("Browser not ready");
|
|
847
|
+
|
|
848
|
+
let events = interactionEventsRepo.findBySession(sessionId, 10000);
|
|
849
|
+
if (fromSequence != null) events = events.filter(e => e.sequence >= fromSequence);
|
|
850
|
+
if (toSequence != null) events = events.filter(e => e.sequence <= toSequence);
|
|
851
|
+
|
|
852
|
+
if (events.length === 0) return text({ error: "No interactions to replay" });
|
|
853
|
+
|
|
854
|
+
const result = await replayEngine.replay(webContents, events, { speed, skipMoves });
|
|
855
|
+
return text(result);
|
|
856
|
+
},
|
|
857
|
+
);
|
|
858
|
+
|
|
859
|
+
server.registerTool(
|
|
860
|
+
"execute_browser_action",
|
|
861
|
+
{
|
|
862
|
+
description:
|
|
863
|
+
"Execute a single browser action: click an element, type text, scroll, or navigate. " +
|
|
864
|
+
"Use CSS selectors from interaction recordings or get_page_elements to target elements.",
|
|
865
|
+
inputSchema: z.object({
|
|
866
|
+
action: z.enum(['click', 'type', 'scroll', 'navigate']).describe("Action to perform"),
|
|
867
|
+
selector: z.string().optional().describe("CSS selector of target element (for click/type)"),
|
|
868
|
+
text: z.string().optional().describe("Text to type (for 'type' action)"),
|
|
869
|
+
url: z.string().optional().describe("URL to navigate (for 'navigate' action)"),
|
|
870
|
+
x: z.number().optional().describe("X coordinate (for click without selector)"),
|
|
871
|
+
y: z.number().optional().describe("Y coordinate (for click without selector)"),
|
|
872
|
+
scrollDelta: z.number().optional().describe("Scroll delta in pixels (for 'scroll' action, positive=down)"),
|
|
873
|
+
}),
|
|
874
|
+
},
|
|
875
|
+
async ({ action, selector, text: inputText, url, x, y, scrollDelta }) => {
|
|
876
|
+
const webContents = windowManager.getTabManager()?.getActiveWebContents();
|
|
877
|
+
if (!webContents || webContents.isDestroyed()) throw new Error("Browser not ready");
|
|
878
|
+
const result = await replayEngine.executeAction(webContents, {
|
|
879
|
+
type: action, selector, text: inputText, url, x, y, scrollDelta,
|
|
880
|
+
});
|
|
881
|
+
return text(result);
|
|
882
|
+
},
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
server.registerTool(
|
|
886
|
+
"get_page_elements",
|
|
887
|
+
{
|
|
888
|
+
description:
|
|
889
|
+
"Get interactive elements on the current page with their CSS selectors, text content, and bounding boxes. " +
|
|
890
|
+
"Use this to discover what elements are available before executing browser actions.",
|
|
891
|
+
inputSchema: z.object({
|
|
892
|
+
filter: z.enum(['all', 'clickable', 'inputs', 'links', 'buttons']).default('clickable')
|
|
893
|
+
.describe("Element filter: 'clickable' for buttons/links/interactive, 'inputs' for form fields"),
|
|
894
|
+
}),
|
|
895
|
+
},
|
|
896
|
+
async ({ filter }) => {
|
|
897
|
+
const webContents = windowManager.getTabManager()?.getActiveWebContents();
|
|
898
|
+
if (!webContents || webContents.isDestroyed()) throw new Error("Browser not ready");
|
|
899
|
+
|
|
900
|
+
const selectorMap: Record<string, string> = {
|
|
901
|
+
all: 'a, button, input, select, textarea, [role="button"], [onclick], [tabindex]',
|
|
902
|
+
clickable: 'a, button, [role="button"], [onclick], [tabindex]:not(input):not(textarea)',
|
|
903
|
+
inputs: 'input, select, textarea',
|
|
904
|
+
links: 'a[href]',
|
|
905
|
+
buttons: 'button, [role="button"], input[type="submit"], input[type="button"]',
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
const result = await webContents.executeJavaScript(`
|
|
909
|
+
(function() {
|
|
910
|
+
const selector = ${JSON.stringify(selectorMap[filter] || selectorMap.clickable)};
|
|
911
|
+
const elements = Array.from(document.querySelectorAll(selector)).slice(0, 50);
|
|
912
|
+
return elements.map(el => {
|
|
913
|
+
const rect = el.getBoundingClientRect();
|
|
914
|
+
if (rect.width === 0 && rect.height === 0) return null; // hidden
|
|
915
|
+
const id = el.id && !(/[0-9a-f]{8,}|_\\d+$|^:r\\d+:|^ember\\d+/.test(el.id))
|
|
916
|
+
? '#' + el.id : null;
|
|
917
|
+
const testId = el.getAttribute('data-testid');
|
|
918
|
+
const selector = id || (testId ? '[data-testid=\"' + testId + '\"]' : null)
|
|
919
|
+
|| (el.className && typeof el.className === 'string'
|
|
920
|
+
? el.tagName.toLowerCase() + '.' + el.className.trim().split(/\\s+/).slice(0,2).join('.')
|
|
921
|
+
: el.tagName.toLowerCase());
|
|
922
|
+
return {
|
|
923
|
+
selector,
|
|
924
|
+
tag: el.tagName.toLowerCase(),
|
|
925
|
+
type: el.getAttribute('type'),
|
|
926
|
+
text: (el.textContent || '').trim().slice(0, 80),
|
|
927
|
+
placeholder: el.getAttribute('placeholder'),
|
|
928
|
+
href: el.getAttribute('href'),
|
|
929
|
+
rect: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
|
|
930
|
+
};
|
|
931
|
+
}).filter(Boolean);
|
|
932
|
+
})()
|
|
933
|
+
`, true);
|
|
934
|
+
|
|
935
|
+
return text(result);
|
|
936
|
+
},
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// ---- Resource Registration ----
|
|
941
|
+
|
|
942
|
+
function registerResources(server: McpServer, deps: MCPServerDeps): void {
|
|
943
|
+
const { sessionManager, windowManager } = deps;
|
|
944
|
+
|
|
945
|
+
server.registerResource(
|
|
946
|
+
"sessions",
|
|
947
|
+
"sessions://list",
|
|
948
|
+
{
|
|
949
|
+
description: "List of all analysis sessions",
|
|
950
|
+
},
|
|
951
|
+
async (uri) => ({
|
|
952
|
+
contents: [
|
|
953
|
+
{
|
|
954
|
+
uri: uri.href,
|
|
955
|
+
text: JSON.stringify(sessionManager.listSessions(), null, 2),
|
|
956
|
+
mimeType: "application/json",
|
|
957
|
+
},
|
|
958
|
+
],
|
|
959
|
+
}),
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
server.registerResource(
|
|
963
|
+
"app-status",
|
|
964
|
+
"app://status",
|
|
965
|
+
{
|
|
966
|
+
description:
|
|
967
|
+
"Current application status including active session and capture state",
|
|
968
|
+
},
|
|
969
|
+
async (uri) => ({
|
|
970
|
+
contents: [
|
|
971
|
+
{
|
|
972
|
+
uri: uri.href,
|
|
973
|
+
text: JSON.stringify(
|
|
974
|
+
{
|
|
975
|
+
currentSessionId: sessionManager.getCurrentSessionId(),
|
|
976
|
+
mcpServerRunning: isMCPServerRunning(),
|
|
977
|
+
},
|
|
978
|
+
null,
|
|
979
|
+
2,
|
|
980
|
+
),
|
|
981
|
+
mimeType: "application/json",
|
|
982
|
+
},
|
|
983
|
+
],
|
|
984
|
+
}),
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
server.registerResource(
|
|
988
|
+
"browser-tabs",
|
|
989
|
+
"browser://tabs",
|
|
990
|
+
{
|
|
991
|
+
description: "Current browser tabs",
|
|
992
|
+
},
|
|
993
|
+
async (uri) => {
|
|
994
|
+
const tabs =
|
|
995
|
+
windowManager
|
|
996
|
+
.getTabManager()
|
|
997
|
+
?.getAllTabs()
|
|
998
|
+
.map((t) => ({
|
|
999
|
+
id: t.id,
|
|
1000
|
+
url: t.url,
|
|
1001
|
+
title: t.title,
|
|
1002
|
+
})) || [];
|
|
1003
|
+
return {
|
|
1004
|
+
contents: [
|
|
1005
|
+
{
|
|
1006
|
+
uri: uri.href,
|
|
1007
|
+
text: JSON.stringify(tabs, null, 2),
|
|
1008
|
+
mimeType: "application/json",
|
|
1009
|
+
},
|
|
1010
|
+
],
|
|
1011
|
+
};
|
|
1012
|
+
},
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// ---- Helpers ----
|
|
1017
|
+
|
|
1018
|
+
function text(data: unknown) {
|
|
1019
|
+
return {
|
|
1020
|
+
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function readBody(req: IncomingMessage): Promise<unknown> {
|
|
1025
|
+
return new Promise((resolve, reject) => {
|
|
1026
|
+
const chunks: Buffer[] = [];
|
|
1027
|
+
req.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
1028
|
+
req.on("end", () => {
|
|
1029
|
+
try {
|
|
1030
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
1031
|
+
resolve(body ? JSON.parse(body) : undefined);
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
reject(err);
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
req.on("error", reject);
|
|
1037
|
+
});
|
|
1038
|
+
}
|