@squidlerio/squidler-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -0
- package/dist/cli-hhgt3239.js +370 -0
- package/dist/cli-hreaeftx.js +374 -0
- package/dist/cli-w466m345.js +377 -0
- package/dist/cli.js +46 -0
- package/dist/mcp-proxy-server.js +532 -0
- package/package.json +57 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
VERSION,
|
|
4
|
+
authenticateWithOAuth,
|
|
5
|
+
downloadChrome,
|
|
6
|
+
loadStoredAuth
|
|
7
|
+
} from "./cli-w466m345.js";
|
|
8
|
+
|
|
9
|
+
// src/mcp-proxy-server.ts
|
|
10
|
+
import { setMaxListeners } from "events";
|
|
11
|
+
|
|
12
|
+
// src/mcp-proxy.ts
|
|
13
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
14
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
15
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
+
import {
|
|
18
|
+
CallToolRequestSchema,
|
|
19
|
+
ListToolsRequestSchema,
|
|
20
|
+
ListResourcesRequestSchema,
|
|
21
|
+
ReadResourceRequestSchema,
|
|
22
|
+
ListPromptsRequestSchema,
|
|
23
|
+
GetPromptRequestSchema
|
|
24
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
25
|
+
|
|
26
|
+
// src/cdp/session.ts
|
|
27
|
+
import WebSocket from "ws";
|
|
28
|
+
|
|
29
|
+
// src/chrome/launcher.ts
|
|
30
|
+
import { spawn } from "child_process";
|
|
31
|
+
import * as fs from "fs";
|
|
32
|
+
import * as path from "path";
|
|
33
|
+
import * as os from "os";
|
|
34
|
+
import * as net from "net";
|
|
35
|
+
async function findFreePort() {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const server = net.createServer();
|
|
38
|
+
server.unref();
|
|
39
|
+
server.on("error", reject);
|
|
40
|
+
server.listen(0, "127.0.0.1", () => {
|
|
41
|
+
const address = server.address();
|
|
42
|
+
if (address && typeof address === "object") {
|
|
43
|
+
const port = address.port;
|
|
44
|
+
server.close(() => resolve(port));
|
|
45
|
+
} else {
|
|
46
|
+
reject(new Error("Could not get port"));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function extractWsEndpoint(process2) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const timeout = setTimeout(() => {
|
|
54
|
+
reject(new Error("Timeout waiting for Chrome to start"));
|
|
55
|
+
}, 30000);
|
|
56
|
+
const pattern = /DevTools listening on (ws:\/\/[^\s]+)/;
|
|
57
|
+
const onData = (data) => {
|
|
58
|
+
const text = data.toString();
|
|
59
|
+
const match = pattern.exec(text);
|
|
60
|
+
if (match) {
|
|
61
|
+
clearTimeout(timeout);
|
|
62
|
+
process2.stderr?.off("data", onData);
|
|
63
|
+
process2.stdout?.off("data", onData);
|
|
64
|
+
resolve(match[1]);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
process2.stderr?.on("data", onData);
|
|
68
|
+
process2.stdout?.on("data", onData);
|
|
69
|
+
process2.on("error", (err) => {
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
reject(err);
|
|
72
|
+
});
|
|
73
|
+
process2.on("exit", (code) => {
|
|
74
|
+
clearTimeout(timeout);
|
|
75
|
+
if (code !== 0) {
|
|
76
|
+
reject(new Error(`Chrome exited with code ${code}`));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async function launchChrome(options = {}) {
|
|
82
|
+
let executablePath = options.executablePath;
|
|
83
|
+
if (!executablePath) {
|
|
84
|
+
const chromeInfo = await downloadChrome();
|
|
85
|
+
executablePath = chromeInfo.executablePath;
|
|
86
|
+
}
|
|
87
|
+
const port = options.port || await findFreePort();
|
|
88
|
+
const userDataDir = options.userDataDir || fs.mkdtempSync(path.join(os.tmpdir(), "squidler-chrome-"));
|
|
89
|
+
const args = [
|
|
90
|
+
`--remote-debugging-port=${port}`,
|
|
91
|
+
"--remote-debugging-address=127.0.0.1",
|
|
92
|
+
`--user-data-dir=${userDataDir}`,
|
|
93
|
+
"--no-first-run",
|
|
94
|
+
"--no-default-browser-check",
|
|
95
|
+
"--disable-background-networking",
|
|
96
|
+
"--disable-background-timer-throttling",
|
|
97
|
+
"--disable-backgrounding-occluded-windows",
|
|
98
|
+
"--disable-breakpad",
|
|
99
|
+
"--disable-client-side-phishing-detection",
|
|
100
|
+
"--disable-component-update",
|
|
101
|
+
"--disable-default-apps",
|
|
102
|
+
"--disable-dev-shm-usage",
|
|
103
|
+
"--disable-domain-reliability",
|
|
104
|
+
"--disable-extensions",
|
|
105
|
+
"--disable-hang-monitor",
|
|
106
|
+
"--disable-ipc-flooding-protection",
|
|
107
|
+
"--disable-popup-blocking",
|
|
108
|
+
"--disable-prompt-on-repost",
|
|
109
|
+
"--disable-renderer-backgrounding",
|
|
110
|
+
"--disable-sync",
|
|
111
|
+
"--disable-translate",
|
|
112
|
+
"--disable-web-security",
|
|
113
|
+
"--disable-site-isolation-trials",
|
|
114
|
+
"--metrics-recording-only",
|
|
115
|
+
"--mute-audio",
|
|
116
|
+
"--no-sandbox",
|
|
117
|
+
"--safebrowsing-disable-auto-update",
|
|
118
|
+
`--window-size=${options.width || 1920},${options.height || 1080}`
|
|
119
|
+
];
|
|
120
|
+
if (options.headless !== false) {
|
|
121
|
+
args.push("--headless=new");
|
|
122
|
+
}
|
|
123
|
+
if (options.args) {
|
|
124
|
+
args.push(...options.args);
|
|
125
|
+
}
|
|
126
|
+
console.error(`Launching Chrome on port ${port}...`);
|
|
127
|
+
const chromeProcess = spawn(executablePath, args, {
|
|
128
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
129
|
+
detached: false
|
|
130
|
+
});
|
|
131
|
+
const wsEndpoint = await extractWsEndpoint(chromeProcess);
|
|
132
|
+
console.error(`Chrome started: ${wsEndpoint}`);
|
|
133
|
+
const close = async () => {
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
if (chromeProcess.killed) {
|
|
136
|
+
resolve();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
chromeProcess.on("exit", () => {
|
|
140
|
+
try {
|
|
141
|
+
fs.rmSync(userDataDir, { recursive: true, force: true });
|
|
142
|
+
} catch {}
|
|
143
|
+
resolve();
|
|
144
|
+
});
|
|
145
|
+
chromeProcess.kill("SIGTERM");
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
if (!chromeProcess.killed) {
|
|
148
|
+
chromeProcess.kill("SIGKILL");
|
|
149
|
+
}
|
|
150
|
+
}, 5000);
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
const pid = chromeProcess.pid;
|
|
154
|
+
if (pid === undefined) {
|
|
155
|
+
throw new Error("Chrome process started but has no PID - this should not happen");
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
wsEndpoint,
|
|
159
|
+
pid,
|
|
160
|
+
port,
|
|
161
|
+
process: chromeProcess,
|
|
162
|
+
userDataDir,
|
|
163
|
+
close
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/cdp/session.ts
|
|
168
|
+
var activeSession = null;
|
|
169
|
+
async function createProxySession(cdpProxyBaseUrl) {
|
|
170
|
+
const url = `${cdpProxyBaseUrl.replace(/^ws/, "http")}/session`;
|
|
171
|
+
const response = await fetch(url, { method: "POST" });
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
throw new Error(`Failed to create CDP proxy session: ${response.status} ${response.statusText}`);
|
|
174
|
+
}
|
|
175
|
+
return await response.json();
|
|
176
|
+
}
|
|
177
|
+
function waitForOpen(ws, name) {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
const timeout = setTimeout(() => {
|
|
180
|
+
reject(new Error(`Timeout connecting to ${name}`));
|
|
181
|
+
}, 30000);
|
|
182
|
+
ws.on("open", () => {
|
|
183
|
+
clearTimeout(timeout);
|
|
184
|
+
resolve();
|
|
185
|
+
});
|
|
186
|
+
ws.on("error", (err) => {
|
|
187
|
+
clearTimeout(timeout);
|
|
188
|
+
reject(new Error(`Failed to connect to ${name}: ${err.message}`));
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async function startSession(cdpProxyBaseUrl, chromeOptions) {
|
|
193
|
+
if (activeSession) {
|
|
194
|
+
throw new Error("Session already active. Stop it first.");
|
|
195
|
+
}
|
|
196
|
+
console.error("Querying CDP proxy for Chrome version...");
|
|
197
|
+
const versionsUrl = `${cdpProxyBaseUrl.replace(/^ws/, "http")}/versions`;
|
|
198
|
+
const versionsResponse = await fetch(versionsUrl);
|
|
199
|
+
if (!versionsResponse.ok) {
|
|
200
|
+
throw new Error(`Failed to query CDP proxy versions: ${versionsResponse.status}`);
|
|
201
|
+
}
|
|
202
|
+
const versions = await versionsResponse.json();
|
|
203
|
+
console.error(`Required Chrome version: ${versions.chrome}`);
|
|
204
|
+
if (versions.mcpServer !== VERSION) {
|
|
205
|
+
console.error(`WARNING: CDP proxy expects MCP server ${versions.mcpServer}, running ${VERSION}. Please update your MCP server.`);
|
|
206
|
+
}
|
|
207
|
+
const chromeInfo = await downloadChrome({ version: versions.chrome });
|
|
208
|
+
console.error(`Chrome ready: ${chromeInfo.version}`);
|
|
209
|
+
console.error("Creating CDP proxy session...");
|
|
210
|
+
const sessionInfo = await createProxySession(cdpProxyBaseUrl);
|
|
211
|
+
console.error(`Session created: ${sessionInfo.sessionId}`);
|
|
212
|
+
console.error("Launching local Chrome...");
|
|
213
|
+
const chrome = await launchChrome({
|
|
214
|
+
...chromeOptions,
|
|
215
|
+
executablePath: chromeInfo.executablePath
|
|
216
|
+
});
|
|
217
|
+
console.error(`Chrome launched on port ${chrome.port}`);
|
|
218
|
+
let chromeWs;
|
|
219
|
+
let proxyWs;
|
|
220
|
+
try {
|
|
221
|
+
console.error(`Connecting to Chrome: ${chrome.wsEndpoint}`);
|
|
222
|
+
chromeWs = new WebSocket(chrome.wsEndpoint);
|
|
223
|
+
await waitForOpen(chromeWs, "Chrome");
|
|
224
|
+
console.error(`Connecting to CDP proxy: ${sessionInfo.localUrl}`);
|
|
225
|
+
proxyWs = new WebSocket(sessionInfo.localUrl);
|
|
226
|
+
await waitForOpen(proxyWs, "CDP proxy");
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error("Failed to connect WebSockets, cleaning up Chrome...");
|
|
229
|
+
await chrome.close();
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
proxyWs.on("message", (data, isBinary) => {
|
|
233
|
+
if (chromeWs.readyState === WebSocket.OPEN) {
|
|
234
|
+
chromeWs.send(data, { binary: isBinary });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
chromeWs.on("message", (data, isBinary) => {
|
|
238
|
+
if (proxyWs.readyState === WebSocket.OPEN) {
|
|
239
|
+
proxyWs.send(data, { binary: isBinary });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
proxyWs.on("close", () => {
|
|
243
|
+
console.error("CDP proxy connection closed");
|
|
244
|
+
});
|
|
245
|
+
chromeWs.on("close", () => {
|
|
246
|
+
console.error("Chrome connection closed");
|
|
247
|
+
});
|
|
248
|
+
proxyWs.on("error", (err) => {
|
|
249
|
+
console.error("CDP proxy WebSocket error:", err.message);
|
|
250
|
+
});
|
|
251
|
+
chromeWs.on("error", (err) => {
|
|
252
|
+
console.error("Chrome WebSocket error:", err.message);
|
|
253
|
+
});
|
|
254
|
+
const pingInterval = setInterval(() => {
|
|
255
|
+
if (proxyWs.readyState === WebSocket.OPEN)
|
|
256
|
+
proxyWs.ping();
|
|
257
|
+
if (chromeWs.readyState === WebSocket.OPEN)
|
|
258
|
+
chromeWs.ping();
|
|
259
|
+
}, 30000);
|
|
260
|
+
activeSession = {
|
|
261
|
+
sessionId: sessionInfo.sessionId,
|
|
262
|
+
localUrl: sessionInfo.localUrl,
|
|
263
|
+
remoteUrl: sessionInfo.remoteUrl,
|
|
264
|
+
chrome,
|
|
265
|
+
chromeWs,
|
|
266
|
+
proxyWs,
|
|
267
|
+
pingInterval,
|
|
268
|
+
headless: chromeOptions?.headless !== false
|
|
269
|
+
};
|
|
270
|
+
console.error("CDP session established");
|
|
271
|
+
return activeSession;
|
|
272
|
+
}
|
|
273
|
+
async function stopSession() {
|
|
274
|
+
if (!activeSession)
|
|
275
|
+
return;
|
|
276
|
+
const session = activeSession;
|
|
277
|
+
activeSession = null;
|
|
278
|
+
console.error("Stopping CDP session...");
|
|
279
|
+
if (session.pingInterval) {
|
|
280
|
+
clearInterval(session.pingInterval);
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
session.proxyWs?.close(1000, "Session stopped");
|
|
284
|
+
} catch {}
|
|
285
|
+
try {
|
|
286
|
+
session.chromeWs?.close(1000, "Session stopped");
|
|
287
|
+
} catch {}
|
|
288
|
+
try {
|
|
289
|
+
await session.chrome?.close();
|
|
290
|
+
} catch {}
|
|
291
|
+
console.error("CDP session stopped");
|
|
292
|
+
}
|
|
293
|
+
function getActiveSession() {
|
|
294
|
+
return activeSession;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/mcp-proxy.ts
|
|
298
|
+
function toolResult(text, isError) {
|
|
299
|
+
return {
|
|
300
|
+
content: [{ type: "text", text }],
|
|
301
|
+
...isError ? { isError: true } : {}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function deriveCdpProxyUrl(apiUrl) {
|
|
305
|
+
const url = new URL(apiUrl);
|
|
306
|
+
const parts = url.hostname.split(".");
|
|
307
|
+
if (["api", "mcp"].includes(parts[0])) {
|
|
308
|
+
parts[0] = "cdp-proxy";
|
|
309
|
+
} else {
|
|
310
|
+
parts.splice(0, 0, "cdp-proxy");
|
|
311
|
+
}
|
|
312
|
+
return `wss://${parts.join(".")}`;
|
|
313
|
+
}
|
|
314
|
+
async function startMCPProxy(options) {
|
|
315
|
+
const { apiUrl } = options;
|
|
316
|
+
const mcpUrl = apiUrl;
|
|
317
|
+
const cdpProxyUrl = deriveCdpProxyUrl(apiUrl);
|
|
318
|
+
let localChromeSettings = null;
|
|
319
|
+
let remoteClient = null;
|
|
320
|
+
let resolvedApiKey;
|
|
321
|
+
async function getApiKey() {
|
|
322
|
+
if (resolvedApiKey)
|
|
323
|
+
return resolvedApiKey;
|
|
324
|
+
if (options.apiKey) {
|
|
325
|
+
resolvedApiKey = options.apiKey;
|
|
326
|
+
} else if (options.resolveApiKey) {
|
|
327
|
+
resolvedApiKey = await options.resolveApiKey();
|
|
328
|
+
} else {
|
|
329
|
+
throw new Error("No API key configured. Set SQUIDLER_API_KEY or run: squidler-mcp login");
|
|
330
|
+
}
|
|
331
|
+
return resolvedApiKey;
|
|
332
|
+
}
|
|
333
|
+
async function connectRemote() {
|
|
334
|
+
const apiKey = await getApiKey();
|
|
335
|
+
const transport = new StreamableHTTPClientTransport(new URL(mcpUrl), {
|
|
336
|
+
requestInit: {
|
|
337
|
+
headers: {
|
|
338
|
+
Authorization: `Bearer ${apiKey}`,
|
|
339
|
+
"X-Squidler-Client": "npm-proxy"
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
const client = new Client({
|
|
344
|
+
name: "squidler-local-proxy",
|
|
345
|
+
version: VERSION
|
|
346
|
+
});
|
|
347
|
+
client.onerror = (error) => {
|
|
348
|
+
console.error("Remote MCP client error:", error.message);
|
|
349
|
+
};
|
|
350
|
+
client.onclose = () => {
|
|
351
|
+
console.error("Remote MCP connection closed");
|
|
352
|
+
};
|
|
353
|
+
await client.connect(transport);
|
|
354
|
+
console.error("Connected to remote MCP server");
|
|
355
|
+
return client;
|
|
356
|
+
}
|
|
357
|
+
async function ensureRemote() {
|
|
358
|
+
if (!remoteClient) {
|
|
359
|
+
remoteClient = await connectRemote();
|
|
360
|
+
}
|
|
361
|
+
return remoteClient;
|
|
362
|
+
}
|
|
363
|
+
async function withReconnect(fn) {
|
|
364
|
+
const client = await ensureRemote();
|
|
365
|
+
try {
|
|
366
|
+
return await fn(client);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
369
|
+
if (msg.includes("Session not found") || msg.includes("fetch failed")) {
|
|
370
|
+
console.error(`Remote session lost (${msg}), reconnecting...`);
|
|
371
|
+
try {
|
|
372
|
+
await client.close();
|
|
373
|
+
} catch {}
|
|
374
|
+
remoteClient = await connectRemote();
|
|
375
|
+
return await fn(remoteClient);
|
|
376
|
+
}
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async function runWithLocalChrome(request) {
|
|
381
|
+
const existingSession = getActiveSession();
|
|
382
|
+
if (existingSession) {
|
|
383
|
+
console.error("Recycling CDP session for clean test environment...");
|
|
384
|
+
await stopSession();
|
|
385
|
+
}
|
|
386
|
+
const freshSession = await startSession(cdpProxyUrl, {
|
|
387
|
+
headless: localChromeSettings.headless
|
|
388
|
+
});
|
|
389
|
+
return await withReconnect((c) => c.callTool({
|
|
390
|
+
name: request.params.name,
|
|
391
|
+
arguments: {
|
|
392
|
+
...request.params.arguments,
|
|
393
|
+
cdpProxyUrl: freshSession.remoteUrl
|
|
394
|
+
}
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
const localServer = new Server({ name: "squidler", version: VERSION }, {
|
|
398
|
+
capabilities: {
|
|
399
|
+
tools: {},
|
|
400
|
+
resources: {},
|
|
401
|
+
prompts: {}
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
localServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
405
|
+
const remoteResult = await withReconnect((c) => c.listTools());
|
|
406
|
+
const localTools = [
|
|
407
|
+
{
|
|
408
|
+
name: "local_session_start",
|
|
409
|
+
description: "Start a local Chrome session for testing localhost URLs. Launches Chrome and connects it to the CDP proxy so cloud tests can control it.",
|
|
410
|
+
inputSchema: {
|
|
411
|
+
type: "object",
|
|
412
|
+
properties: {
|
|
413
|
+
headless: {
|
|
414
|
+
type: "boolean",
|
|
415
|
+
description: "Run Chrome in headless mode (default: true)"
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: "local_session_stop",
|
|
422
|
+
description: "Stop the local Chrome session and clean up resources.",
|
|
423
|
+
inputSchema: { type: "object", properties: {} }
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: "local_session_status",
|
|
427
|
+
description: "Check the status of the local Chrome session.",
|
|
428
|
+
inputSchema: { type: "object", properties: {} }
|
|
429
|
+
}
|
|
430
|
+
];
|
|
431
|
+
return { tools: [...remoteResult.tools, ...localTools] };
|
|
432
|
+
});
|
|
433
|
+
localServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
434
|
+
const { name } = request.params;
|
|
435
|
+
switch (name) {
|
|
436
|
+
case "local_session_start": {
|
|
437
|
+
const headless = request.params.arguments?.headless !== false;
|
|
438
|
+
localChromeSettings = { headless };
|
|
439
|
+
return toolResult(JSON.stringify({
|
|
440
|
+
status: "enabled",
|
|
441
|
+
headless,
|
|
442
|
+
message: "Local Chrome mode enabled. Tests will now run against your local Chrome."
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
445
|
+
case "local_session_stop": {
|
|
446
|
+
localChromeSettings = null;
|
|
447
|
+
await stopSession();
|
|
448
|
+
return toolResult(JSON.stringify({
|
|
449
|
+
status: "stopped",
|
|
450
|
+
message: "Local Chrome mode disabled and session stopped."
|
|
451
|
+
}));
|
|
452
|
+
}
|
|
453
|
+
case "local_session_status": {
|
|
454
|
+
const session = getActiveSession();
|
|
455
|
+
return toolResult(JSON.stringify({
|
|
456
|
+
localChrome: localChromeSettings ? "enabled" : "disabled",
|
|
457
|
+
...localChromeSettings ?? {},
|
|
458
|
+
session: session ? { status: "active", sessionId: session.sessionId } : { status: "inactive" }
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
case "test_case_run": {
|
|
462
|
+
if (localChromeSettings) {
|
|
463
|
+
return await runWithLocalChrome(request);
|
|
464
|
+
}
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return await withReconnect((c) => c.callTool({
|
|
469
|
+
name,
|
|
470
|
+
arguments: request.params.arguments
|
|
471
|
+
}));
|
|
472
|
+
});
|
|
473
|
+
localServer.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
474
|
+
return await withReconnect((c) => c.listResources());
|
|
475
|
+
});
|
|
476
|
+
localServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
477
|
+
return await withReconnect((c) => c.readResource({ uri: request.params.uri }));
|
|
478
|
+
});
|
|
479
|
+
localServer.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
480
|
+
return await withReconnect((c) => c.listPrompts());
|
|
481
|
+
});
|
|
482
|
+
localServer.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
483
|
+
return await withReconnect((c) => c.getPrompt({
|
|
484
|
+
name: request.params.name,
|
|
485
|
+
arguments: request.params.arguments
|
|
486
|
+
}));
|
|
487
|
+
});
|
|
488
|
+
const stdioTransport = new StdioServerTransport;
|
|
489
|
+
await localServer.connect(stdioTransport);
|
|
490
|
+
console.error("Squidler MCP proxy started");
|
|
491
|
+
console.error(`Remote: ${apiUrl}`);
|
|
492
|
+
console.error(`CDP Proxy: ${cdpProxyUrl}`);
|
|
493
|
+
process.on("SIGINT", async () => {
|
|
494
|
+
await stopSession();
|
|
495
|
+
await localServer.close();
|
|
496
|
+
await remoteClient?.close();
|
|
497
|
+
process.exit(0);
|
|
498
|
+
});
|
|
499
|
+
process.on("SIGTERM", async () => {
|
|
500
|
+
await stopSession();
|
|
501
|
+
await localServer.close();
|
|
502
|
+
await remoteClient?.close();
|
|
503
|
+
process.exit(0);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/mcp-proxy-server.ts
|
|
508
|
+
setMaxListeners(0);
|
|
509
|
+
var SQUIDLER_API_URL = process.env.SQUIDLER_API_URL || "https://mcp.squidler.io";
|
|
510
|
+
async function main() {
|
|
511
|
+
await startMCPProxy({
|
|
512
|
+
apiUrl: SQUIDLER_API_URL,
|
|
513
|
+
resolveApiKey: async () => {
|
|
514
|
+
const envKey = process.env.SQUIDLER_API_KEY;
|
|
515
|
+
if (envKey)
|
|
516
|
+
return envKey;
|
|
517
|
+
const stored = loadStoredAuth(SQUIDLER_API_URL);
|
|
518
|
+
if (stored) {
|
|
519
|
+
console.error("Using stored authentication token");
|
|
520
|
+
return stored.access_token;
|
|
521
|
+
}
|
|
522
|
+
console.error("No API key found. Starting browser authentication...");
|
|
523
|
+
const token = await authenticateWithOAuth(SQUIDLER_API_URL);
|
|
524
|
+
console.error("Authentication successful!");
|
|
525
|
+
return token;
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
main().catch((error) => {
|
|
530
|
+
console.error("Fatal error:", error);
|
|
531
|
+
process.exit(1);
|
|
532
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@squidlerio/squidler-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Squidler MCP proxy - enables testing localhost URLs via local Chrome and CDP proxy",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/squidlerio/mcp.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/squidlerio/mcp",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/squidlerio/mcp/issues"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"squidler-mcp": "dist/cli.js",
|
|
19
|
+
"squidler-mcp-proxy": "dist/mcp-proxy-server.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"package.json",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "bun build src/cli.ts src/mcp-proxy-server.ts --outdir dist --target node --splitting --packages external",
|
|
28
|
+
"start": "bun src/cli.ts",
|
|
29
|
+
"mcp-proxy": "bun src/mcp-proxy-server.ts",
|
|
30
|
+
"clean": "rm -rf dist"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"squidler",
|
|
34
|
+
"chrome",
|
|
35
|
+
"cdp",
|
|
36
|
+
"testing",
|
|
37
|
+
"localhost",
|
|
38
|
+
"mcp",
|
|
39
|
+
"model-context-protocol"
|
|
40
|
+
],
|
|
41
|
+
"author": "Squidler",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.0.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
48
|
+
"commander": "^12.1.0",
|
|
49
|
+
"unzipper": "^0.12.3",
|
|
50
|
+
"ws": "^8.18.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^22.10.2",
|
|
54
|
+
"@types/unzipper": "^0.10.10",
|
|
55
|
+
"@types/ws": "^8.5.13"
|
|
56
|
+
}
|
|
57
|
+
}
|