@ai-ecoverse/mcpcat 0.15.4 → 0.15.6
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/dist/host/mcpcat.mjs +210 -88
- package/dist/kernel/mcpcat.mjs +240 -98
- package/package.json +3 -3
package/dist/host/mcpcat.mjs
CHANGED
|
@@ -5329,8 +5329,8 @@ function parseCodemodeSource(input) {
|
|
|
5329
5329
|
throw new CodemodeSourceError(`The ${CODEMODE_OPTIONS_PREFIX} line must be the first line of the input; found one on line ${misplaced + 1}`);
|
|
5330
5330
|
}
|
|
5331
5331
|
const newline = input.indexOf("\n");
|
|
5332
|
-
const
|
|
5333
|
-
const trimmed =
|
|
5332
|
+
const firstLine2 = lines[0].replace(/\r$/, "");
|
|
5333
|
+
const trimmed = firstLine2.trimStart();
|
|
5334
5334
|
if (!trimmed.startsWith(CODEMODE_OPTIONS_PREFIX))
|
|
5335
5335
|
return { code: input, options: {} };
|
|
5336
5336
|
const code2 = newline === -1 ? "" : input.slice(newline);
|
|
@@ -8369,8 +8369,9 @@ var McpTimeoutError2 = class extends Error {
|
|
|
8369
8369
|
var McpAuthError = class extends Error {
|
|
8370
8370
|
status;
|
|
8371
8371
|
target;
|
|
8372
|
-
constructor(status, target) {
|
|
8373
|
-
|
|
8372
|
+
constructor(status, target, reason) {
|
|
8373
|
+
const why = reason ? ` (${reason})` : "";
|
|
8374
|
+
super(`HTTP ${status}: authentication required${why}. Run: mcpcat login ${target}`);
|
|
8374
8375
|
this.name = "McpAuthError";
|
|
8375
8376
|
this.status = status;
|
|
8376
8377
|
this.target = target;
|
|
@@ -8388,12 +8389,23 @@ var TransportRefusalError = class extends Error {
|
|
|
8388
8389
|
};
|
|
8389
8390
|
var AuthRetryableError = class extends Error {
|
|
8390
8391
|
response;
|
|
8391
|
-
|
|
8392
|
+
reason;
|
|
8393
|
+
constructor(response, reason) {
|
|
8392
8394
|
super(`HTTP ${response.status}: auth retry`);
|
|
8393
8395
|
this.name = "AuthRetryableError";
|
|
8394
8396
|
this.response = response;
|
|
8397
|
+
this.reason = reason;
|
|
8395
8398
|
}
|
|
8396
8399
|
};
|
|
8400
|
+
var KERNEL_REFUSAL = "nodeTransport refuses ";
|
|
8401
|
+
function firstLine(text) {
|
|
8402
|
+
return text.trim().split("\n")[0].trim().slice(0, 200);
|
|
8403
|
+
}
|
|
8404
|
+
async function authReason(res) {
|
|
8405
|
+
const type = res.headers.get("content-type") ?? "";
|
|
8406
|
+
if (!type.toLowerCase().startsWith("text/plain")) return void 0;
|
|
8407
|
+
return firstLine(await res.text()) || void 0;
|
|
8408
|
+
}
|
|
8397
8409
|
function selectSseFrame(body, targetId) {
|
|
8398
8410
|
const text = new TextDecoder().decode(body);
|
|
8399
8411
|
const blocks = text.replace(/\r\n/g, "\n").split(/\n\n/);
|
|
@@ -8510,7 +8522,8 @@ async function createStatelessConnection(options) {
|
|
|
8510
8522
|
clientInfo = { name: "mcpcat", version: "0.0.0" },
|
|
8511
8523
|
signal: externalSignal,
|
|
8512
8524
|
verbose,
|
|
8513
|
-
authProvider
|
|
8525
|
+
authProvider,
|
|
8526
|
+
transportRefusals
|
|
8514
8527
|
} = options;
|
|
8515
8528
|
let nextId2 = 1;
|
|
8516
8529
|
let negotiatedVersion;
|
|
@@ -8528,18 +8541,14 @@ async function createStatelessConnection(options) {
|
|
|
8528
8541
|
return h;
|
|
8529
8542
|
}
|
|
8530
8543
|
async function handleErrorStatus(res, method) {
|
|
8531
|
-
if (res.status === 403) {
|
|
8532
|
-
const
|
|
8533
|
-
if (!
|
|
8534
|
-
|
|
8535
|
-
if (body && !body.includes("insufficient_scope")) {
|
|
8536
|
-
throw new TransportRefusalError(403, body.split("\n")[0].slice(0, 200));
|
|
8537
|
-
}
|
|
8544
|
+
if (res.status === 401 || res.status === 403) {
|
|
8545
|
+
const reason = await authReason(res);
|
|
8546
|
+
if (transportRefusals && res.status === 403 && !res.headers.has("www-authenticate") && reason?.startsWith(KERNEL_REFUSAL)) {
|
|
8547
|
+
throw new TransportRefusalError(403, reason);
|
|
8538
8548
|
}
|
|
8549
|
+
if (authProvider) throw new AuthRetryableError(res, reason);
|
|
8550
|
+
throw new McpAuthError(res.status, url, reason);
|
|
8539
8551
|
}
|
|
8540
|
-
if ((res.status === 401 || res.status === 403) && authProvider)
|
|
8541
|
-
throw new AuthRetryableError(res);
|
|
8542
|
-
if (res.status === 401 || res.status === 403) throw new McpAuthError(res.status, url);
|
|
8543
8552
|
const text = await res.text();
|
|
8544
8553
|
let rpc;
|
|
8545
8554
|
try {
|
|
@@ -8594,7 +8603,7 @@ async function createStatelessConnection(options) {
|
|
|
8594
8603
|
return frame.result;
|
|
8595
8604
|
}
|
|
8596
8605
|
async function handleAuthError(err, token) {
|
|
8597
|
-
if (!authProvider?.onUnauthorized) throw new McpAuthError(err.response.status, url);
|
|
8606
|
+
if (!authProvider?.onUnauthorized) throw new McpAuthError(err.response.status, url, err.reason);
|
|
8598
8607
|
try {
|
|
8599
8608
|
await authProvider.onUnauthorized({
|
|
8600
8609
|
response: err.response,
|
|
@@ -8604,7 +8613,7 @@ async function createStatelessConnection(options) {
|
|
|
8604
8613
|
});
|
|
8605
8614
|
} catch (authErr) {
|
|
8606
8615
|
if (authErr instanceof AuthStepUpError || authErr instanceof JerkTimeoutError) throw authErr;
|
|
8607
|
-
throw new McpAuthError(err.response.status, url);
|
|
8616
|
+
throw new McpAuthError(err.response.status, url, err.reason);
|
|
8608
8617
|
}
|
|
8609
8618
|
}
|
|
8610
8619
|
async function request(method, params) {
|
|
@@ -9065,7 +9074,8 @@ async function connect(platform2, options) {
|
|
|
9065
9074
|
timeoutMs: options.timeoutMs,
|
|
9066
9075
|
signal: options.signal,
|
|
9067
9076
|
verbose: options.verbose,
|
|
9068
|
-
authProvider: options.authProvider
|
|
9077
|
+
authProvider: options.authProvider,
|
|
9078
|
+
transportRefusals: platform2.kind === "kernel"
|
|
9069
9079
|
});
|
|
9070
9080
|
} catch (err) {
|
|
9071
9081
|
const isMethodNotFound = err instanceof StatelessRpcError && err.code === -32601 || err instanceof StatelessHttpError && err.status >= 400 && err.status < 500;
|
|
@@ -12183,16 +12193,45 @@ function wrapUntrusted(content) {
|
|
|
12183
12193
|
${neutralised}
|
|
12184
12194
|
${closeTag}`;
|
|
12185
12195
|
}
|
|
12186
|
-
|
|
12196
|
+
var WEBMCP_LIST_FLOOR = 1048576;
|
|
12197
|
+
function capCollector(limit) {
|
|
12198
|
+
const dec = new TextDecoder();
|
|
12199
|
+
const half = Math.floor(limit / 2);
|
|
12200
|
+
let head = "";
|
|
12201
|
+
let tail = "";
|
|
12202
|
+
let total = 0;
|
|
12203
|
+
const take = (piece) => {
|
|
12204
|
+
if (!piece) return;
|
|
12205
|
+
total += piece.length;
|
|
12206
|
+
if (head.length < limit) head += piece.slice(0, limit - head.length);
|
|
12207
|
+
tail = half > 0 ? (tail + piece).slice(-half) : "";
|
|
12208
|
+
};
|
|
12209
|
+
return {
|
|
12210
|
+
push(chunk) {
|
|
12211
|
+
take(dec.decode(chunk, { stream: true }));
|
|
12212
|
+
},
|
|
12213
|
+
finish() {
|
|
12214
|
+
take(dec.decode());
|
|
12215
|
+
if (total <= limit) return { text: head, cut: false };
|
|
12216
|
+
return {
|
|
12217
|
+
text: `${head.slice(0, half)}
|
|
12218
|
+
\u2026 ${total - limit} characters cut \u2026
|
|
12219
|
+
${tail}`,
|
|
12220
|
+
cut: true
|
|
12221
|
+
};
|
|
12222
|
+
}
|
|
12223
|
+
};
|
|
12224
|
+
}
|
|
12225
|
+
async function runProcess(platform2, argv, stdin, timeoutMs, limit) {
|
|
12187
12226
|
let proc;
|
|
12188
12227
|
try {
|
|
12189
12228
|
proc = await platform2.spawn({ argv });
|
|
12190
|
-
} catch (
|
|
12191
|
-
const code2 =
|
|
12229
|
+
} catch (err2) {
|
|
12230
|
+
const code2 = err2.code;
|
|
12192
12231
|
if (code2 === "ENOENT" || code2 === "EACCES") {
|
|
12193
|
-
return { stdout: "", stderr: `command not found: ${argv[0]}`, exitCode: -1 };
|
|
12232
|
+
return { stdout: "", stderr: `command not found: ${argv[0]}`, exitCode: -1, cut: false };
|
|
12194
12233
|
}
|
|
12195
|
-
throw
|
|
12234
|
+
throw err2;
|
|
12196
12235
|
}
|
|
12197
12236
|
if (stdin !== void 0) {
|
|
12198
12237
|
const writer = proc.stdin.getWriter();
|
|
@@ -12204,8 +12243,8 @@ async function runProcess(platform2, argv, stdin, timeoutMs) {
|
|
|
12204
12243
|
} catch {
|
|
12205
12244
|
}
|
|
12206
12245
|
}
|
|
12207
|
-
const
|
|
12208
|
-
const
|
|
12246
|
+
const out = capCollector(limit);
|
|
12247
|
+
const err = capCollector(limit);
|
|
12209
12248
|
const readStream3 = async (stream, into) => {
|
|
12210
12249
|
const r = stream.getReader();
|
|
12211
12250
|
try {
|
|
@@ -12218,53 +12257,136 @@ async function runProcess(platform2, argv, stdin, timeoutMs) {
|
|
|
12218
12257
|
r.releaseLock();
|
|
12219
12258
|
}
|
|
12220
12259
|
};
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12260
|
+
let timeoutId;
|
|
12261
|
+
const timer = new Promise((resolve) => {
|
|
12262
|
+
timeoutId = setTimeout(() => resolve("timeout"), timeoutMs);
|
|
12263
|
+
});
|
|
12224
12264
|
const work = Promise.all([
|
|
12225
|
-
readStream3(proc.stdout,
|
|
12226
|
-
readStream3(proc.stderr,
|
|
12265
|
+
readStream3(proc.stdout, out),
|
|
12266
|
+
readStream3(proc.stderr, err),
|
|
12227
12267
|
proc.wait()
|
|
12228
12268
|
]);
|
|
12229
12269
|
const result = await Promise.race([work, timer]);
|
|
12270
|
+
clearTimeout(timeoutId);
|
|
12230
12271
|
if (result === "timeout") {
|
|
12231
12272
|
await proc.kill("SIGTERM");
|
|
12232
|
-
return { stdout: "", stderr: "timed out", exitCode: 124 };
|
|
12273
|
+
return { stdout: "", stderr: "timed out", exitCode: 124, cut: false };
|
|
12233
12274
|
}
|
|
12234
12275
|
const [, , exitResult] = result;
|
|
12235
|
-
const
|
|
12236
|
-
const
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
offset += c.length;
|
|
12243
|
-
}
|
|
12244
|
-
return out;
|
|
12276
|
+
const stdout = out.finish();
|
|
12277
|
+
const stderr = err.finish();
|
|
12278
|
+
return {
|
|
12279
|
+
stdout: stdout.text,
|
|
12280
|
+
stderr: stderr.text,
|
|
12281
|
+
exitCode: exitResult.status,
|
|
12282
|
+
cut: stdout.cut
|
|
12245
12283
|
};
|
|
12284
|
+
}
|
|
12285
|
+
async function resolveWebMcpTab(platform2, options) {
|
|
12286
|
+
const argv = [options.playwrightCli, "tab-list"];
|
|
12287
|
+
if (options.cdp) argv.push("--cdp", options.cdp);
|
|
12288
|
+
const result = await runProcess(
|
|
12289
|
+
platform2,
|
|
12290
|
+
argv,
|
|
12291
|
+
void 0,
|
|
12292
|
+
options.callTimeoutMs,
|
|
12293
|
+
WEBMCP_LIST_FLOOR
|
|
12294
|
+
);
|
|
12295
|
+
if (result.exitCode === -1) return { error: result.stderr, notFound: true };
|
|
12296
|
+
if (result.exitCode !== 0) {
|
|
12297
|
+
return { error: result.stderr.trim() || `tab-list exited ${result.exitCode}` };
|
|
12298
|
+
}
|
|
12299
|
+
const rows = [];
|
|
12300
|
+
for (const line of result.stdout.split("\n")) {
|
|
12301
|
+
const match = /^(\d+): \[([^\]\s]+)\] /.exec(line);
|
|
12302
|
+
if (!match) continue;
|
|
12303
|
+
if (Number(match[1]) !== rows.length + 1) {
|
|
12304
|
+
return { error: "could not read playwright-cli tab-list" };
|
|
12305
|
+
}
|
|
12306
|
+
rows.push({ id: match[2], active: line.endsWith('" (active)') });
|
|
12307
|
+
}
|
|
12308
|
+
const active = rows.filter((r) => r.active);
|
|
12309
|
+
if (new Set(rows.map((r) => r.id)).size !== rows.length || active.length > 1) {
|
|
12310
|
+
return { error: "could not read playwright-cli tab-list" };
|
|
12311
|
+
}
|
|
12312
|
+
if (active.length === 1) return { tab: active[0].id };
|
|
12313
|
+
if (rows.length === 1) return { tab: rows[0].id };
|
|
12246
12314
|
return {
|
|
12247
|
-
|
|
12248
|
-
stderr: dec.decode(concat2(stderrChunks)),
|
|
12249
|
-
exitCode: exitResult.status
|
|
12315
|
+
error: rows.length === 0 ? "no browser tab is open; open one or pass --tab <targetId>" : "no current playwright-cli tab; pass --tab <targetId> (see playwright-cli tab-list)"
|
|
12250
12316
|
};
|
|
12251
12317
|
}
|
|
12318
|
+
function mapDescriptionAnnotations(ann) {
|
|
12319
|
+
const mapped = { openWorldHint: true };
|
|
12320
|
+
if (!ann) return mapped;
|
|
12321
|
+
if (ann.readOnlyHint !== void 0) mapped.readOnlyHint = ann.readOnlyHint;
|
|
12322
|
+
if (ann.consequentialHint) mapped.destructiveHint = true;
|
|
12323
|
+
return mapped;
|
|
12324
|
+
}
|
|
12325
|
+
function originPrefix(tools) {
|
|
12326
|
+
const origins = new Set(tools.map((t) => t.origin));
|
|
12327
|
+
return origins.size > 1;
|
|
12328
|
+
}
|
|
12329
|
+
function hostFromOrigin(origin) {
|
|
12330
|
+
try {
|
|
12331
|
+
return new URL(origin).host;
|
|
12332
|
+
} catch {
|
|
12333
|
+
return origin;
|
|
12334
|
+
}
|
|
12335
|
+
}
|
|
12336
|
+
function buildDescription(tool) {
|
|
12337
|
+
const parts = [];
|
|
12338
|
+
if (tool.description) parts.push(tool.description);
|
|
12339
|
+
parts.push(`[origin: ${tool.origin}]`);
|
|
12340
|
+
return parts.join("\n");
|
|
12341
|
+
}
|
|
12342
|
+
function buildToolName(tool, prefix) {
|
|
12343
|
+
const base = tool.name;
|
|
12344
|
+
if (!prefix) return base;
|
|
12345
|
+
const host = hostFromOrigin(tool.origin).replace(/[^a-zA-Z0-9_]/g, "_");
|
|
12346
|
+
return `${host}__${base}`;
|
|
12347
|
+
}
|
|
12348
|
+
function exposedTools(tools) {
|
|
12349
|
+
const needPrefix = originPrefix(tools);
|
|
12350
|
+
const byBase = /* @__PURE__ */ new Map();
|
|
12351
|
+
const byName = /* @__PURE__ */ new Map();
|
|
12352
|
+
for (const tool of tools) {
|
|
12353
|
+
const name = buildToolName(tool, needPrefix);
|
|
12354
|
+
byBase.set(tool.name, [...byBase.get(tool.name) ?? [], tool]);
|
|
12355
|
+
byName.set(name, [...byName.get(name) ?? [], tool]);
|
|
12356
|
+
}
|
|
12357
|
+
return tools.map((tool) => {
|
|
12358
|
+
const name = buildToolName(tool, needPrefix);
|
|
12359
|
+
const sameBase = byBase.get(tool.name);
|
|
12360
|
+
const sameName = byName.get(name);
|
|
12361
|
+
return { name, tool, peers: sameBase.length > 1 ? sameBase : sameName };
|
|
12362
|
+
});
|
|
12363
|
+
}
|
|
12364
|
+
function ambiguityError(name, peers) {
|
|
12365
|
+
const origins = [...new Set(peers.map((t) => String(t.origin)))].join(", ");
|
|
12366
|
+
return new Error(
|
|
12367
|
+
`tool "${name}" is ambiguous: ${peers.length} tools on the page match it (${origins}), and playwright-cli runs the first tool by name and cannot select one by origin, so mcpcat refuses the call`
|
|
12368
|
+
);
|
|
12369
|
+
}
|
|
12252
12370
|
function createWebMcpHandler(platform2, options) {
|
|
12253
12371
|
let cache;
|
|
12254
12372
|
const baseArgs = buildBaseArgs(options);
|
|
12255
|
-
|
|
12373
|
+
const listLimit = Math.max(options.maxOutput, WEBMCP_LIST_FLOOR);
|
|
12374
|
+
async function listRaw(fresh = false) {
|
|
12256
12375
|
const now = Date.now();
|
|
12257
|
-
if (cache && now < cache.expiresAt) {
|
|
12376
|
+
if (!fresh && cache && now < cache.expiresAt) {
|
|
12258
12377
|
return { tools: cache.tools };
|
|
12259
12378
|
}
|
|
12260
12379
|
const argv = [options.playwrightCli, "webmcp-list", "--json", ...baseArgs];
|
|
12261
|
-
const result = await runProcess(platform2, argv, void 0, options.callTimeoutMs);
|
|
12380
|
+
const result = await runProcess(platform2, argv, void 0, options.callTimeoutMs, listLimit);
|
|
12262
12381
|
if (result.exitCode === -1) {
|
|
12263
12382
|
return { tools: [], error: result.stderr, notFound: true };
|
|
12264
12383
|
}
|
|
12265
12384
|
if (result.exitCode !== 0) {
|
|
12266
12385
|
return { tools: [], error: result.stderr || `webmcp-list exited ${result.exitCode}` };
|
|
12267
12386
|
}
|
|
12387
|
+
if (result.cut) {
|
|
12388
|
+
return { tools: [], error: `webmcp-list output exceeds ${listLimit} characters` };
|
|
12389
|
+
}
|
|
12268
12390
|
let parsed;
|
|
12269
12391
|
try {
|
|
12270
12392
|
parsed = JSON.parse(result.stdout);
|
|
@@ -12278,49 +12400,19 @@ function createWebMcpHandler(platform2, options) {
|
|
|
12278
12400
|
cache = { tools, expiresAt: now + options.ttlMs };
|
|
12279
12401
|
return { tools };
|
|
12280
12402
|
}
|
|
12281
|
-
function mapDescriptionAnnotations(ann) {
|
|
12282
|
-
const mapped = { openWorldHint: true };
|
|
12283
|
-
if (!ann) return mapped;
|
|
12284
|
-
if (ann.readOnlyHint !== void 0) mapped.readOnlyHint = ann.readOnlyHint;
|
|
12285
|
-
if (ann.consequentialHint) mapped.destructiveHint = true;
|
|
12286
|
-
return mapped;
|
|
12287
|
-
}
|
|
12288
|
-
function originPrefix(tools) {
|
|
12289
|
-
const origins = new Set(tools.map((t) => t.origin));
|
|
12290
|
-
return origins.size > 1;
|
|
12291
|
-
}
|
|
12292
|
-
function hostFromOrigin(origin) {
|
|
12293
|
-
try {
|
|
12294
|
-
return new URL(origin).host;
|
|
12295
|
-
} catch {
|
|
12296
|
-
return origin;
|
|
12297
|
-
}
|
|
12298
|
-
}
|
|
12299
|
-
function buildDescription(tool) {
|
|
12300
|
-
const parts = [];
|
|
12301
|
-
if (tool.description) parts.push(tool.description);
|
|
12302
|
-
parts.push(`[origin: ${tool.origin}]`);
|
|
12303
|
-
return parts.join("\n");
|
|
12304
|
-
}
|
|
12305
|
-
function buildToolName(tool, prefix) {
|
|
12306
|
-
const base = tool.name;
|
|
12307
|
-
if (!prefix) return base;
|
|
12308
|
-
const host = hostFromOrigin(tool.origin).replace(/[^a-zA-Z0-9_]/g, "_");
|
|
12309
|
-
return `${host}__${base}`;
|
|
12310
|
-
}
|
|
12311
12403
|
async function getRpcTools() {
|
|
12312
12404
|
const { tools, error, notFound } = await listRaw();
|
|
12313
12405
|
if (notFound) return null;
|
|
12314
12406
|
if (error) return [];
|
|
12315
|
-
const needPrefix = originPrefix(tools);
|
|
12316
12407
|
const rpcTools = [];
|
|
12317
|
-
for (const tool of tools) {
|
|
12408
|
+
for (const { name, tool, peers } of exposedTools(tools)) {
|
|
12409
|
+
if (peers.length > 1) continue;
|
|
12318
12410
|
if (tool.annotations?.consequentialHint && !options.allowConsequential) {
|
|
12319
12411
|
continue;
|
|
12320
12412
|
}
|
|
12321
12413
|
const mapped = mapDescriptionAnnotations(tool.annotations);
|
|
12322
12414
|
rpcTools.push({
|
|
12323
|
-
name
|
|
12415
|
+
name,
|
|
12324
12416
|
description: buildDescription(tool),
|
|
12325
12417
|
inputSchema: tool.inputSchema,
|
|
12326
12418
|
annotations: mapped
|
|
@@ -12329,21 +12421,30 @@ function createWebMcpHandler(platform2, options) {
|
|
|
12329
12421
|
return rpcTools;
|
|
12330
12422
|
}
|
|
12331
12423
|
async function callTool(name, args2) {
|
|
12332
|
-
const { tools, error } = await listRaw();
|
|
12424
|
+
const { tools, error } = await listRaw(true);
|
|
12333
12425
|
if (error) {
|
|
12334
12426
|
return { stdout: "", stderr: error, exitCode: 2 };
|
|
12335
12427
|
}
|
|
12336
|
-
const
|
|
12337
|
-
|
|
12338
|
-
if (!tool) {
|
|
12428
|
+
const match = exposedTools(tools).find((t) => t.name === name);
|
|
12429
|
+
if (!match) {
|
|
12339
12430
|
return { stdout: "", stderr: `unknown tool: ${name}`, exitCode: 2 };
|
|
12340
12431
|
}
|
|
12432
|
+
if (match.peers.length > 1) {
|
|
12433
|
+
throw ambiguityError(name, match.peers);
|
|
12434
|
+
}
|
|
12435
|
+
const { tool } = match;
|
|
12341
12436
|
const argv = [options.playwrightCli, "webmcp-call", tool.name, "-", ...baseArgs];
|
|
12342
12437
|
if (options.allowConsequential) {
|
|
12343
12438
|
argv.push("--allow-consequential");
|
|
12344
12439
|
}
|
|
12345
12440
|
const stdinData = JSON.stringify(args2 ?? {});
|
|
12346
|
-
const result = await runProcess(
|
|
12441
|
+
const result = await runProcess(
|
|
12442
|
+
platform2,
|
|
12443
|
+
argv,
|
|
12444
|
+
stdinData,
|
|
12445
|
+
options.callTimeoutMs,
|
|
12446
|
+
options.maxOutput
|
|
12447
|
+
);
|
|
12347
12448
|
if (result.exitCode === -1) {
|
|
12348
12449
|
throw new Error(result.stderr || "playwright-cli not found");
|
|
12349
12450
|
}
|
|
@@ -13094,9 +13195,30 @@ async function runWebMcpServe(platform2, options, version, resolvedBearer, gener
|
|
|
13094
13195
|
return 3;
|
|
13095
13196
|
}
|
|
13096
13197
|
const cdp = options.webmcpCdp ?? platform2.env.SLICC_CDP_URL;
|
|
13198
|
+
let tab = options.webmcpTab;
|
|
13199
|
+
if (!tab) {
|
|
13200
|
+
const resolved = await resolveWebMcpTab(platform2, {
|
|
13201
|
+
playwrightCli: cliPath,
|
|
13202
|
+
cdp,
|
|
13203
|
+
callTimeoutMs: options.callTimeout
|
|
13204
|
+
});
|
|
13205
|
+
if (resolved.notFound) {
|
|
13206
|
+
await platform2.write(
|
|
13207
|
+
2,
|
|
13208
|
+
"mcpcat serve --webmcp: playwright-cli not found. Install it with: curl -fsSL https://get.sliccy.ai/playwright-cli | sh\n"
|
|
13209
|
+
);
|
|
13210
|
+
return 3;
|
|
13211
|
+
}
|
|
13212
|
+
if (!resolved.tab) {
|
|
13213
|
+
await platform2.write(2, `mcpcat serve --webmcp: ${resolved.error}
|
|
13214
|
+
`);
|
|
13215
|
+
return 3;
|
|
13216
|
+
}
|
|
13217
|
+
tab = resolved.tab;
|
|
13218
|
+
}
|
|
13097
13219
|
const handler = createWebMcpHandler(platform2, {
|
|
13098
13220
|
playwrightCli: cliPath,
|
|
13099
|
-
tab
|
|
13221
|
+
tab,
|
|
13100
13222
|
cdp,
|
|
13101
13223
|
allowConsequential: options.webmcpAllowConsequential,
|
|
13102
13224
|
ttlMs: options.webmcpTtl,
|
|
@@ -16734,5 +16856,5 @@ try {
|
|
|
16734
16856
|
});
|
|
16735
16857
|
} catch {
|
|
16736
16858
|
}
|
|
16737
|
-
var code = await run(platform, "0.15.
|
|
16859
|
+
var code = await run(platform, "0.15.6", args);
|
|
16738
16860
|
process.exit(code);
|
package/dist/kernel/mcpcat.mjs
CHANGED
|
@@ -4950,8 +4950,8 @@ function parseCodemodeSource(input) {
|
|
|
4950
4950
|
throw new CodemodeSourceError(`The ${CODEMODE_OPTIONS_PREFIX} line must be the first line of the input; found one on line ${misplaced + 1}`);
|
|
4951
4951
|
}
|
|
4952
4952
|
const newline = input.indexOf("\n");
|
|
4953
|
-
const
|
|
4954
|
-
const trimmed =
|
|
4953
|
+
const firstLine2 = lines[0].replace(/\r$/, "");
|
|
4954
|
+
const trimmed = firstLine2.trimStart();
|
|
4955
4955
|
if (!trimmed.startsWith(CODEMODE_OPTIONS_PREFIX))
|
|
4956
4956
|
return { code: input, options: {} };
|
|
4957
4957
|
const code = newline === -1 ? "" : input.slice(newline);
|
|
@@ -7990,8 +7990,9 @@ var McpTimeoutError2 = class extends Error {
|
|
|
7990
7990
|
var McpAuthError = class extends Error {
|
|
7991
7991
|
status;
|
|
7992
7992
|
target;
|
|
7993
|
-
constructor(status, target) {
|
|
7994
|
-
|
|
7993
|
+
constructor(status, target, reason) {
|
|
7994
|
+
const why = reason ? ` (${reason})` : "";
|
|
7995
|
+
super(`HTTP ${status}: authentication required${why}. Run: mcpcat login ${target}`);
|
|
7995
7996
|
this.name = "McpAuthError";
|
|
7996
7997
|
this.status = status;
|
|
7997
7998
|
this.target = target;
|
|
@@ -8009,12 +8010,23 @@ var TransportRefusalError = class extends Error {
|
|
|
8009
8010
|
};
|
|
8010
8011
|
var AuthRetryableError = class extends Error {
|
|
8011
8012
|
response;
|
|
8012
|
-
|
|
8013
|
+
reason;
|
|
8014
|
+
constructor(response, reason) {
|
|
8013
8015
|
super(`HTTP ${response.status}: auth retry`);
|
|
8014
8016
|
this.name = "AuthRetryableError";
|
|
8015
8017
|
this.response = response;
|
|
8018
|
+
this.reason = reason;
|
|
8016
8019
|
}
|
|
8017
8020
|
};
|
|
8021
|
+
var KERNEL_REFUSAL = "nodeTransport refuses ";
|
|
8022
|
+
function firstLine(text) {
|
|
8023
|
+
return text.trim().split("\n")[0].trim().slice(0, 200);
|
|
8024
|
+
}
|
|
8025
|
+
async function authReason(res) {
|
|
8026
|
+
const type = res.headers.get("content-type") ?? "";
|
|
8027
|
+
if (!type.toLowerCase().startsWith("text/plain")) return void 0;
|
|
8028
|
+
return firstLine(await res.text()) || void 0;
|
|
8029
|
+
}
|
|
8018
8030
|
function selectSseFrame(body, targetId) {
|
|
8019
8031
|
const text = new TextDecoder().decode(body);
|
|
8020
8032
|
const blocks = text.replace(/\r\n/g, "\n").split(/\n\n/);
|
|
@@ -8131,7 +8143,8 @@ async function createStatelessConnection(options) {
|
|
|
8131
8143
|
clientInfo = { name: "mcpcat", version: "0.0.0" },
|
|
8132
8144
|
signal: externalSignal,
|
|
8133
8145
|
verbose,
|
|
8134
|
-
authProvider
|
|
8146
|
+
authProvider,
|
|
8147
|
+
transportRefusals
|
|
8135
8148
|
} = options;
|
|
8136
8149
|
let nextId2 = 1;
|
|
8137
8150
|
let negotiatedVersion;
|
|
@@ -8149,18 +8162,14 @@ async function createStatelessConnection(options) {
|
|
|
8149
8162
|
return h;
|
|
8150
8163
|
}
|
|
8151
8164
|
async function handleErrorStatus(res, method) {
|
|
8152
|
-
if (res.status === 403) {
|
|
8153
|
-
const
|
|
8154
|
-
if (!
|
|
8155
|
-
|
|
8156
|
-
if (body && !body.includes("insufficient_scope")) {
|
|
8157
|
-
throw new TransportRefusalError(403, body.split("\n")[0].slice(0, 200));
|
|
8158
|
-
}
|
|
8165
|
+
if (res.status === 401 || res.status === 403) {
|
|
8166
|
+
const reason = await authReason(res);
|
|
8167
|
+
if (transportRefusals && res.status === 403 && !res.headers.has("www-authenticate") && reason?.startsWith(KERNEL_REFUSAL)) {
|
|
8168
|
+
throw new TransportRefusalError(403, reason);
|
|
8159
8169
|
}
|
|
8170
|
+
if (authProvider) throw new AuthRetryableError(res, reason);
|
|
8171
|
+
throw new McpAuthError(res.status, url, reason);
|
|
8160
8172
|
}
|
|
8161
|
-
if ((res.status === 401 || res.status === 403) && authProvider)
|
|
8162
|
-
throw new AuthRetryableError(res);
|
|
8163
|
-
if (res.status === 401 || res.status === 403) throw new McpAuthError(res.status, url);
|
|
8164
8173
|
const text = await res.text();
|
|
8165
8174
|
let rpc;
|
|
8166
8175
|
try {
|
|
@@ -8215,7 +8224,7 @@ async function createStatelessConnection(options) {
|
|
|
8215
8224
|
return frame.result;
|
|
8216
8225
|
}
|
|
8217
8226
|
async function handleAuthError(err, token) {
|
|
8218
|
-
if (!authProvider?.onUnauthorized) throw new McpAuthError(err.response.status, url);
|
|
8227
|
+
if (!authProvider?.onUnauthorized) throw new McpAuthError(err.response.status, url, err.reason);
|
|
8219
8228
|
try {
|
|
8220
8229
|
await authProvider.onUnauthorized({
|
|
8221
8230
|
response: err.response,
|
|
@@ -8225,7 +8234,7 @@ async function createStatelessConnection(options) {
|
|
|
8225
8234
|
});
|
|
8226
8235
|
} catch (authErr) {
|
|
8227
8236
|
if (authErr instanceof AuthStepUpError || authErr instanceof JerkTimeoutError) throw authErr;
|
|
8228
|
-
throw new McpAuthError(err.response.status, url);
|
|
8237
|
+
throw new McpAuthError(err.response.status, url, err.reason);
|
|
8229
8238
|
}
|
|
8230
8239
|
}
|
|
8231
8240
|
async function request(method, params) {
|
|
@@ -8686,7 +8695,8 @@ async function connect(platform2, options) {
|
|
|
8686
8695
|
timeoutMs: options.timeoutMs,
|
|
8687
8696
|
signal: options.signal,
|
|
8688
8697
|
verbose: options.verbose,
|
|
8689
|
-
authProvider: options.authProvider
|
|
8698
|
+
authProvider: options.authProvider,
|
|
8699
|
+
transportRefusals: platform2.kind === "kernel"
|
|
8690
8700
|
});
|
|
8691
8701
|
} catch (err) {
|
|
8692
8702
|
const isMethodNotFound = err instanceof StatelessRpcError && err.code === -32601 || err instanceof StatelessHttpError && err.status >= 400 && err.status < 500;
|
|
@@ -11804,16 +11814,45 @@ function wrapUntrusted(content) {
|
|
|
11804
11814
|
${neutralised}
|
|
11805
11815
|
${closeTag}`;
|
|
11806
11816
|
}
|
|
11807
|
-
|
|
11817
|
+
var WEBMCP_LIST_FLOOR = 1048576;
|
|
11818
|
+
function capCollector(limit) {
|
|
11819
|
+
const dec = new TextDecoder();
|
|
11820
|
+
const half = Math.floor(limit / 2);
|
|
11821
|
+
let head = "";
|
|
11822
|
+
let tail = "";
|
|
11823
|
+
let total = 0;
|
|
11824
|
+
const take = (piece) => {
|
|
11825
|
+
if (!piece) return;
|
|
11826
|
+
total += piece.length;
|
|
11827
|
+
if (head.length < limit) head += piece.slice(0, limit - head.length);
|
|
11828
|
+
tail = half > 0 ? (tail + piece).slice(-half) : "";
|
|
11829
|
+
};
|
|
11830
|
+
return {
|
|
11831
|
+
push(chunk) {
|
|
11832
|
+
take(dec.decode(chunk, { stream: true }));
|
|
11833
|
+
},
|
|
11834
|
+
finish() {
|
|
11835
|
+
take(dec.decode());
|
|
11836
|
+
if (total <= limit) return { text: head, cut: false };
|
|
11837
|
+
return {
|
|
11838
|
+
text: `${head.slice(0, half)}
|
|
11839
|
+
\u2026 ${total - limit} characters cut \u2026
|
|
11840
|
+
${tail}`,
|
|
11841
|
+
cut: true
|
|
11842
|
+
};
|
|
11843
|
+
}
|
|
11844
|
+
};
|
|
11845
|
+
}
|
|
11846
|
+
async function runProcess(platform2, argv, stdin, timeoutMs, limit) {
|
|
11808
11847
|
let proc;
|
|
11809
11848
|
try {
|
|
11810
11849
|
proc = await platform2.spawn({ argv });
|
|
11811
|
-
} catch (
|
|
11812
|
-
const code =
|
|
11850
|
+
} catch (err2) {
|
|
11851
|
+
const code = err2.code;
|
|
11813
11852
|
if (code === "ENOENT" || code === "EACCES") {
|
|
11814
|
-
return { stdout: "", stderr: `command not found: ${argv[0]}`, exitCode: -1 };
|
|
11853
|
+
return { stdout: "", stderr: `command not found: ${argv[0]}`, exitCode: -1, cut: false };
|
|
11815
11854
|
}
|
|
11816
|
-
throw
|
|
11855
|
+
throw err2;
|
|
11817
11856
|
}
|
|
11818
11857
|
if (stdin !== void 0) {
|
|
11819
11858
|
const writer = proc.stdin.getWriter();
|
|
@@ -11825,8 +11864,8 @@ async function runProcess(platform2, argv, stdin, timeoutMs) {
|
|
|
11825
11864
|
} catch {
|
|
11826
11865
|
}
|
|
11827
11866
|
}
|
|
11828
|
-
const
|
|
11829
|
-
const
|
|
11867
|
+
const out = capCollector(limit);
|
|
11868
|
+
const err = capCollector(limit);
|
|
11830
11869
|
const readStream3 = async (stream, into) => {
|
|
11831
11870
|
const r = stream.getReader();
|
|
11832
11871
|
try {
|
|
@@ -11839,53 +11878,136 @@ async function runProcess(platform2, argv, stdin, timeoutMs) {
|
|
|
11839
11878
|
r.releaseLock();
|
|
11840
11879
|
}
|
|
11841
11880
|
};
|
|
11842
|
-
|
|
11843
|
-
|
|
11844
|
-
|
|
11881
|
+
let timeoutId;
|
|
11882
|
+
const timer = new Promise((resolve2) => {
|
|
11883
|
+
timeoutId = setTimeout(() => resolve2("timeout"), timeoutMs);
|
|
11884
|
+
});
|
|
11845
11885
|
const work = Promise.all([
|
|
11846
|
-
readStream3(proc.stdout,
|
|
11847
|
-
readStream3(proc.stderr,
|
|
11886
|
+
readStream3(proc.stdout, out),
|
|
11887
|
+
readStream3(proc.stderr, err),
|
|
11848
11888
|
proc.wait()
|
|
11849
11889
|
]);
|
|
11850
11890
|
const result = await Promise.race([work, timer]);
|
|
11891
|
+
clearTimeout(timeoutId);
|
|
11851
11892
|
if (result === "timeout") {
|
|
11852
11893
|
await proc.kill("SIGTERM");
|
|
11853
|
-
return { stdout: "", stderr: "timed out", exitCode: 124 };
|
|
11894
|
+
return { stdout: "", stderr: "timed out", exitCode: 124, cut: false };
|
|
11854
11895
|
}
|
|
11855
11896
|
const [, , exitResult] = result;
|
|
11856
|
-
const
|
|
11857
|
-
const
|
|
11858
|
-
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
offset += c.length;
|
|
11864
|
-
}
|
|
11865
|
-
return out;
|
|
11897
|
+
const stdout = out.finish();
|
|
11898
|
+
const stderr = err.finish();
|
|
11899
|
+
return {
|
|
11900
|
+
stdout: stdout.text,
|
|
11901
|
+
stderr: stderr.text,
|
|
11902
|
+
exitCode: exitResult.status,
|
|
11903
|
+
cut: stdout.cut
|
|
11866
11904
|
};
|
|
11905
|
+
}
|
|
11906
|
+
async function resolveWebMcpTab(platform2, options) {
|
|
11907
|
+
const argv = [options.playwrightCli, "tab-list"];
|
|
11908
|
+
if (options.cdp) argv.push("--cdp", options.cdp);
|
|
11909
|
+
const result = await runProcess(
|
|
11910
|
+
platform2,
|
|
11911
|
+
argv,
|
|
11912
|
+
void 0,
|
|
11913
|
+
options.callTimeoutMs,
|
|
11914
|
+
WEBMCP_LIST_FLOOR
|
|
11915
|
+
);
|
|
11916
|
+
if (result.exitCode === -1) return { error: result.stderr, notFound: true };
|
|
11917
|
+
if (result.exitCode !== 0) {
|
|
11918
|
+
return { error: result.stderr.trim() || `tab-list exited ${result.exitCode}` };
|
|
11919
|
+
}
|
|
11920
|
+
const rows = [];
|
|
11921
|
+
for (const line of result.stdout.split("\n")) {
|
|
11922
|
+
const match = /^(\d+): \[([^\]\s]+)\] /.exec(line);
|
|
11923
|
+
if (!match) continue;
|
|
11924
|
+
if (Number(match[1]) !== rows.length + 1) {
|
|
11925
|
+
return { error: "could not read playwright-cli tab-list" };
|
|
11926
|
+
}
|
|
11927
|
+
rows.push({ id: match[2], active: line.endsWith('" (active)') });
|
|
11928
|
+
}
|
|
11929
|
+
const active = rows.filter((r) => r.active);
|
|
11930
|
+
if (new Set(rows.map((r) => r.id)).size !== rows.length || active.length > 1) {
|
|
11931
|
+
return { error: "could not read playwright-cli tab-list" };
|
|
11932
|
+
}
|
|
11933
|
+
if (active.length === 1) return { tab: active[0].id };
|
|
11934
|
+
if (rows.length === 1) return { tab: rows[0].id };
|
|
11867
11935
|
return {
|
|
11868
|
-
|
|
11869
|
-
stderr: dec.decode(concat2(stderrChunks)),
|
|
11870
|
-
exitCode: exitResult.status
|
|
11936
|
+
error: rows.length === 0 ? "no browser tab is open; open one or pass --tab <targetId>" : "no current playwright-cli tab; pass --tab <targetId> (see playwright-cli tab-list)"
|
|
11871
11937
|
};
|
|
11872
11938
|
}
|
|
11939
|
+
function mapDescriptionAnnotations(ann) {
|
|
11940
|
+
const mapped = { openWorldHint: true };
|
|
11941
|
+
if (!ann) return mapped;
|
|
11942
|
+
if (ann.readOnlyHint !== void 0) mapped.readOnlyHint = ann.readOnlyHint;
|
|
11943
|
+
if (ann.consequentialHint) mapped.destructiveHint = true;
|
|
11944
|
+
return mapped;
|
|
11945
|
+
}
|
|
11946
|
+
function originPrefix(tools) {
|
|
11947
|
+
const origins = new Set(tools.map((t) => t.origin));
|
|
11948
|
+
return origins.size > 1;
|
|
11949
|
+
}
|
|
11950
|
+
function hostFromOrigin(origin) {
|
|
11951
|
+
try {
|
|
11952
|
+
return new URL(origin).host;
|
|
11953
|
+
} catch {
|
|
11954
|
+
return origin;
|
|
11955
|
+
}
|
|
11956
|
+
}
|
|
11957
|
+
function buildDescription(tool) {
|
|
11958
|
+
const parts = [];
|
|
11959
|
+
if (tool.description) parts.push(tool.description);
|
|
11960
|
+
parts.push(`[origin: ${tool.origin}]`);
|
|
11961
|
+
return parts.join("\n");
|
|
11962
|
+
}
|
|
11963
|
+
function buildToolName(tool, prefix) {
|
|
11964
|
+
const base = tool.name;
|
|
11965
|
+
if (!prefix) return base;
|
|
11966
|
+
const host = hostFromOrigin(tool.origin).replace(/[^a-zA-Z0-9_]/g, "_");
|
|
11967
|
+
return `${host}__${base}`;
|
|
11968
|
+
}
|
|
11969
|
+
function exposedTools(tools) {
|
|
11970
|
+
const needPrefix = originPrefix(tools);
|
|
11971
|
+
const byBase = /* @__PURE__ */ new Map();
|
|
11972
|
+
const byName = /* @__PURE__ */ new Map();
|
|
11973
|
+
for (const tool of tools) {
|
|
11974
|
+
const name = buildToolName(tool, needPrefix);
|
|
11975
|
+
byBase.set(tool.name, [...byBase.get(tool.name) ?? [], tool]);
|
|
11976
|
+
byName.set(name, [...byName.get(name) ?? [], tool]);
|
|
11977
|
+
}
|
|
11978
|
+
return tools.map((tool) => {
|
|
11979
|
+
const name = buildToolName(tool, needPrefix);
|
|
11980
|
+
const sameBase = byBase.get(tool.name);
|
|
11981
|
+
const sameName = byName.get(name);
|
|
11982
|
+
return { name, tool, peers: sameBase.length > 1 ? sameBase : sameName };
|
|
11983
|
+
});
|
|
11984
|
+
}
|
|
11985
|
+
function ambiguityError(name, peers) {
|
|
11986
|
+
const origins = [...new Set(peers.map((t) => String(t.origin)))].join(", ");
|
|
11987
|
+
return new Error(
|
|
11988
|
+
`tool "${name}" is ambiguous: ${peers.length} tools on the page match it (${origins}), and playwright-cli runs the first tool by name and cannot select one by origin, so mcpcat refuses the call`
|
|
11989
|
+
);
|
|
11990
|
+
}
|
|
11873
11991
|
function createWebMcpHandler(platform2, options) {
|
|
11874
11992
|
let cache;
|
|
11875
11993
|
const baseArgs = buildBaseArgs(options);
|
|
11876
|
-
|
|
11994
|
+
const listLimit = Math.max(options.maxOutput, WEBMCP_LIST_FLOOR);
|
|
11995
|
+
async function listRaw(fresh = false) {
|
|
11877
11996
|
const now = Date.now();
|
|
11878
|
-
if (cache && now < cache.expiresAt) {
|
|
11997
|
+
if (!fresh && cache && now < cache.expiresAt) {
|
|
11879
11998
|
return { tools: cache.tools };
|
|
11880
11999
|
}
|
|
11881
12000
|
const argv = [options.playwrightCli, "webmcp-list", "--json", ...baseArgs];
|
|
11882
|
-
const result = await runProcess(platform2, argv, void 0, options.callTimeoutMs);
|
|
12001
|
+
const result = await runProcess(platform2, argv, void 0, options.callTimeoutMs, listLimit);
|
|
11883
12002
|
if (result.exitCode === -1) {
|
|
11884
12003
|
return { tools: [], error: result.stderr, notFound: true };
|
|
11885
12004
|
}
|
|
11886
12005
|
if (result.exitCode !== 0) {
|
|
11887
12006
|
return { tools: [], error: result.stderr || `webmcp-list exited ${result.exitCode}` };
|
|
11888
12007
|
}
|
|
12008
|
+
if (result.cut) {
|
|
12009
|
+
return { tools: [], error: `webmcp-list output exceeds ${listLimit} characters` };
|
|
12010
|
+
}
|
|
11889
12011
|
let parsed;
|
|
11890
12012
|
try {
|
|
11891
12013
|
parsed = JSON.parse(result.stdout);
|
|
@@ -11899,49 +12021,19 @@ function createWebMcpHandler(platform2, options) {
|
|
|
11899
12021
|
cache = { tools, expiresAt: now + options.ttlMs };
|
|
11900
12022
|
return { tools };
|
|
11901
12023
|
}
|
|
11902
|
-
function mapDescriptionAnnotations(ann) {
|
|
11903
|
-
const mapped = { openWorldHint: true };
|
|
11904
|
-
if (!ann) return mapped;
|
|
11905
|
-
if (ann.readOnlyHint !== void 0) mapped.readOnlyHint = ann.readOnlyHint;
|
|
11906
|
-
if (ann.consequentialHint) mapped.destructiveHint = true;
|
|
11907
|
-
return mapped;
|
|
11908
|
-
}
|
|
11909
|
-
function originPrefix(tools) {
|
|
11910
|
-
const origins = new Set(tools.map((t) => t.origin));
|
|
11911
|
-
return origins.size > 1;
|
|
11912
|
-
}
|
|
11913
|
-
function hostFromOrigin(origin) {
|
|
11914
|
-
try {
|
|
11915
|
-
return new URL(origin).host;
|
|
11916
|
-
} catch {
|
|
11917
|
-
return origin;
|
|
11918
|
-
}
|
|
11919
|
-
}
|
|
11920
|
-
function buildDescription(tool) {
|
|
11921
|
-
const parts = [];
|
|
11922
|
-
if (tool.description) parts.push(tool.description);
|
|
11923
|
-
parts.push(`[origin: ${tool.origin}]`);
|
|
11924
|
-
return parts.join("\n");
|
|
11925
|
-
}
|
|
11926
|
-
function buildToolName(tool, prefix) {
|
|
11927
|
-
const base = tool.name;
|
|
11928
|
-
if (!prefix) return base;
|
|
11929
|
-
const host = hostFromOrigin(tool.origin).replace(/[^a-zA-Z0-9_]/g, "_");
|
|
11930
|
-
return `${host}__${base}`;
|
|
11931
|
-
}
|
|
11932
12024
|
async function getRpcTools() {
|
|
11933
12025
|
const { tools, error, notFound } = await listRaw();
|
|
11934
12026
|
if (notFound) return null;
|
|
11935
12027
|
if (error) return [];
|
|
11936
|
-
const needPrefix = originPrefix(tools);
|
|
11937
12028
|
const rpcTools = [];
|
|
11938
|
-
for (const tool of tools) {
|
|
12029
|
+
for (const { name, tool, peers } of exposedTools(tools)) {
|
|
12030
|
+
if (peers.length > 1) continue;
|
|
11939
12031
|
if (tool.annotations?.consequentialHint && !options.allowConsequential) {
|
|
11940
12032
|
continue;
|
|
11941
12033
|
}
|
|
11942
12034
|
const mapped = mapDescriptionAnnotations(tool.annotations);
|
|
11943
12035
|
rpcTools.push({
|
|
11944
|
-
name
|
|
12036
|
+
name,
|
|
11945
12037
|
description: buildDescription(tool),
|
|
11946
12038
|
inputSchema: tool.inputSchema,
|
|
11947
12039
|
annotations: mapped
|
|
@@ -11950,21 +12042,30 @@ function createWebMcpHandler(platform2, options) {
|
|
|
11950
12042
|
return rpcTools;
|
|
11951
12043
|
}
|
|
11952
12044
|
async function callTool(name, args) {
|
|
11953
|
-
const { tools, error } = await listRaw();
|
|
12045
|
+
const { tools, error } = await listRaw(true);
|
|
11954
12046
|
if (error) {
|
|
11955
12047
|
return { stdout: "", stderr: error, exitCode: 2 };
|
|
11956
12048
|
}
|
|
11957
|
-
const
|
|
11958
|
-
|
|
11959
|
-
if (!tool) {
|
|
12049
|
+
const match = exposedTools(tools).find((t) => t.name === name);
|
|
12050
|
+
if (!match) {
|
|
11960
12051
|
return { stdout: "", stderr: `unknown tool: ${name}`, exitCode: 2 };
|
|
11961
12052
|
}
|
|
12053
|
+
if (match.peers.length > 1) {
|
|
12054
|
+
throw ambiguityError(name, match.peers);
|
|
12055
|
+
}
|
|
12056
|
+
const { tool } = match;
|
|
11962
12057
|
const argv = [options.playwrightCli, "webmcp-call", tool.name, "-", ...baseArgs];
|
|
11963
12058
|
if (options.allowConsequential) {
|
|
11964
12059
|
argv.push("--allow-consequential");
|
|
11965
12060
|
}
|
|
11966
12061
|
const stdinData = JSON.stringify(args ?? {});
|
|
11967
|
-
const result = await runProcess(
|
|
12062
|
+
const result = await runProcess(
|
|
12063
|
+
platform2,
|
|
12064
|
+
argv,
|
|
12065
|
+
stdinData,
|
|
12066
|
+
options.callTimeoutMs,
|
|
12067
|
+
options.maxOutput
|
|
12068
|
+
);
|
|
11968
12069
|
if (result.exitCode === -1) {
|
|
11969
12070
|
throw new Error(result.stderr || "playwright-cli not found");
|
|
11970
12071
|
}
|
|
@@ -12715,9 +12816,30 @@ async function runWebMcpServe(platform2, options, version, resolvedBearer, gener
|
|
|
12715
12816
|
return 3;
|
|
12716
12817
|
}
|
|
12717
12818
|
const cdp = options.webmcpCdp ?? platform2.env.SLICC_CDP_URL;
|
|
12819
|
+
let tab = options.webmcpTab;
|
|
12820
|
+
if (!tab) {
|
|
12821
|
+
const resolved = await resolveWebMcpTab(platform2, {
|
|
12822
|
+
playwrightCli: cliPath,
|
|
12823
|
+
cdp,
|
|
12824
|
+
callTimeoutMs: options.callTimeout
|
|
12825
|
+
});
|
|
12826
|
+
if (resolved.notFound) {
|
|
12827
|
+
await platform2.write(
|
|
12828
|
+
2,
|
|
12829
|
+
"mcpcat serve --webmcp: playwright-cli not found. Install it with: curl -fsSL https://get.sliccy.ai/playwright-cli | sh\n"
|
|
12830
|
+
);
|
|
12831
|
+
return 3;
|
|
12832
|
+
}
|
|
12833
|
+
if (!resolved.tab) {
|
|
12834
|
+
await platform2.write(2, `mcpcat serve --webmcp: ${resolved.error}
|
|
12835
|
+
`);
|
|
12836
|
+
return 3;
|
|
12837
|
+
}
|
|
12838
|
+
tab = resolved.tab;
|
|
12839
|
+
}
|
|
12718
12840
|
const handler = createWebMcpHandler(platform2, {
|
|
12719
12841
|
playwrightCli: cliPath,
|
|
12720
|
-
tab
|
|
12842
|
+
tab,
|
|
12721
12843
|
cdp,
|
|
12722
12844
|
allowConsequential: options.webmcpAllowConsequential,
|
|
12723
12845
|
ttlMs: options.webmcpTtl,
|
|
@@ -16251,6 +16373,15 @@ function isConnectionRefused(err) {
|
|
|
16251
16373
|
if (e?.code === "ECONNREFUSED") return true;
|
|
16252
16374
|
return typeof e?.message === "string" && e.message.includes("ECONNREFUSED");
|
|
16253
16375
|
}
|
|
16376
|
+
function isFetchRefused(err) {
|
|
16377
|
+
return err instanceof TypeError && isConnectionRefused(err.cause);
|
|
16378
|
+
}
|
|
16379
|
+
function loopbackRefused(url, port, err) {
|
|
16380
|
+
return new TypeError(
|
|
16381
|
+
`nothing listens on ${url.hostname}:${port} for a kernel program, whose loopback is the kernel's; reach a host server via http://host.slicc.internal:${port}`,
|
|
16382
|
+
{ cause: err.cause }
|
|
16383
|
+
);
|
|
16384
|
+
}
|
|
16254
16385
|
function collectHeaders(init) {
|
|
16255
16386
|
const entries = [];
|
|
16256
16387
|
if (!init?.headers) return entries;
|
|
@@ -16265,6 +16396,15 @@ function collectHeaders(init) {
|
|
|
16265
16396
|
}
|
|
16266
16397
|
return entries;
|
|
16267
16398
|
}
|
|
16399
|
+
async function encodeBody(body, headerEntries) {
|
|
16400
|
+
if (body == null) return new Uint8Array(0);
|
|
16401
|
+
const encoded = new Response(body);
|
|
16402
|
+
const type = encoded.headers.get("content-type");
|
|
16403
|
+
if (type && !headerEntries.some(([k]) => k.toLowerCase() === "content-type")) {
|
|
16404
|
+
headerEntries.push(["Content-Type", type]);
|
|
16405
|
+
}
|
|
16406
|
+
return new Uint8Array(await encoded.arrayBuffer());
|
|
16407
|
+
}
|
|
16268
16408
|
function buildHttpRequest(method, url, headerEntries, bodyBytes) {
|
|
16269
16409
|
const enc = new TextEncoder();
|
|
16270
16410
|
const has = (n) => headerEntries.some(([k]) => k.toLowerCase() === n.toLowerCase());
|
|
@@ -16307,15 +16447,7 @@ function parseHttpResponse(buf, headerPos, bufLen) {
|
|
|
16307
16447
|
}
|
|
16308
16448
|
async function kernelLoopbackFetch(ctx, url, port, init, onRefused) {
|
|
16309
16449
|
const method = (init?.method ?? "GET").toUpperCase();
|
|
16310
|
-
const enc = new TextEncoder();
|
|
16311
16450
|
const dec = new TextDecoder();
|
|
16312
|
-
let bodyStr = "";
|
|
16313
|
-
if (init?.body != null) {
|
|
16314
|
-
bodyStr = typeof init.body === "string" ? init.body : dec.decode(init.body);
|
|
16315
|
-
}
|
|
16316
|
-
const bodyBytes = enc.encode(bodyStr);
|
|
16317
|
-
const headerEntries = collectHeaders(init);
|
|
16318
|
-
const reqBytes = buildHttpRequest(method, url, headerEntries, bodyBytes);
|
|
16319
16451
|
let conn;
|
|
16320
16452
|
try {
|
|
16321
16453
|
conn = await ctx.net.connect({ host: "127.0.0.1", port });
|
|
@@ -16323,8 +16455,10 @@ async function kernelLoopbackFetch(ctx, url, port, init, onRefused) {
|
|
|
16323
16455
|
if (onRefused && isConnectionRefused(err)) return onRefused();
|
|
16324
16456
|
throw err;
|
|
16325
16457
|
}
|
|
16458
|
+
const headerEntries = collectHeaders(init);
|
|
16459
|
+
const bodyBytes = await encodeBody(init?.body, headerEntries);
|
|
16326
16460
|
const writer = conn.writable.getWriter();
|
|
16327
|
-
await writer.write(
|
|
16461
|
+
await writer.write(buildHttpRequest(method, url, headerEntries, bodyBytes));
|
|
16328
16462
|
const reader = conn.readable.getReader();
|
|
16329
16463
|
let buf = new Uint8Array(4096);
|
|
16330
16464
|
let bufLen = 0;
|
|
@@ -16377,7 +16511,15 @@ function createKernelFetch(ctx) {
|
|
|
16377
16511
|
if (port !== null) return kernelLoopbackFetch(ctx, url, port, init);
|
|
16378
16512
|
const plainPort = extractPlainLoopbackPort(url);
|
|
16379
16513
|
if (plainPort !== null) {
|
|
16380
|
-
return kernelLoopbackFetch(
|
|
16514
|
+
return kernelLoopbackFetch(
|
|
16515
|
+
ctx,
|
|
16516
|
+
url,
|
|
16517
|
+
plainPort,
|
|
16518
|
+
init,
|
|
16519
|
+
() => ctx.fetch(input, init).catch((err) => {
|
|
16520
|
+
throw isFetchRefused(err) ? loopbackRefused(url, plainPort, err) : err;
|
|
16521
|
+
})
|
|
16522
|
+
);
|
|
16381
16523
|
}
|
|
16382
16524
|
return ctx.fetch(input, init);
|
|
16383
16525
|
};
|
|
@@ -16435,7 +16577,7 @@ async function main(ctx) {
|
|
|
16435
16577
|
packageDir: "/node_modules/@ai-ecoverse/mcpcat"
|
|
16436
16578
|
});
|
|
16437
16579
|
const args = ctx.argv.slice(1);
|
|
16438
|
-
return run(platform2, "0.15.
|
|
16580
|
+
return run(platform2, "0.15.6", args);
|
|
16439
16581
|
}
|
|
16440
16582
|
export {
|
|
16441
16583
|
main as default
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/mcpcat",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.6",
|
|
4
4
|
"description": "A curl for the Model Context Protocol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"trust:dry": "npx fledgling sync --dry-run -y --skip-publish"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@ai-ecoverse/slicc-kernel": "1.
|
|
60
|
-
"@ai-ecoverse/slicc-shared-web": "1.
|
|
59
|
+
"@ai-ecoverse/slicc-kernel": "1.94.1",
|
|
60
|
+
"@ai-ecoverse/slicc-shared-web": "1.17.0",
|
|
61
61
|
"@biomejs/biome": "2.5.15",
|
|
62
62
|
"@earendil-works/pi-codemode": "1.2.0",
|
|
63
63
|
"@earendil-works/pi-mcp": "1.2.0",
|