@ai-ecoverse/mcpcat 0.15.5 → 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.
@@ -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 firstLine = lines[0].replace(/\r$/, "");
5333
- const trimmed = firstLine.trimStart();
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
- super(`HTTP ${status}: authentication required. Run: mcpcat login ${target}`);
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
- constructor(response) {
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 wwwAuth = res.headers.get("www-authenticate") ?? "";
8533
- if (!wwwAuth) {
8534
- const body = await res.text();
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
- async function runProcess(platform2, argv, stdin, timeoutMs) {
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 (err) {
12191
- const code2 = err.code;
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 err;
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 stdoutChunks = [];
12208
- const stderrChunks = [];
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
- const timer = new Promise(
12222
- (resolve) => setTimeout(() => resolve("timeout"), timeoutMs ?? 3e4)
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, stdoutChunks),
12226
- readStream3(proc.stderr, stderrChunks),
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 dec = new TextDecoder();
12236
- const concat2 = (chunks) => {
12237
- const total = chunks.reduce((s, c) => s + c.length, 0);
12238
- const out = new Uint8Array(total);
12239
- let offset = 0;
12240
- for (const c of chunks) {
12241
- out.set(c, offset);
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
- stdout: dec.decode(concat2(stdoutChunks)),
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
- async function listRaw() {
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: buildToolName(tool, needPrefix),
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 needPrefix = originPrefix(tools);
12337
- const tool = tools.find((t) => buildToolName(t, needPrefix) === name);
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(platform2, argv, stdinData, options.callTimeoutMs);
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: options.webmcpTab,
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.5", args);
16859
+ var code = await run(platform, "0.15.6", args);
16738
16860
  process.exit(code);
@@ -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 firstLine = lines[0].replace(/\r$/, "");
4954
- const trimmed = firstLine.trimStart();
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
- super(`HTTP ${status}: authentication required. Run: mcpcat login ${target}`);
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
- constructor(response) {
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 wwwAuth = res.headers.get("www-authenticate") ?? "";
8154
- if (!wwwAuth) {
8155
- const body = await res.text();
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
- async function runProcess(platform2, argv, stdin, timeoutMs) {
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 (err) {
11812
- const code = err.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 err;
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 stdoutChunks = [];
11829
- const stderrChunks = [];
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
- const timer = new Promise(
11843
- (resolve2) => setTimeout(() => resolve2("timeout"), timeoutMs ?? 3e4)
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, stdoutChunks),
11847
- readStream3(proc.stderr, stderrChunks),
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 dec = new TextDecoder();
11857
- const concat2 = (chunks) => {
11858
- const total = chunks.reduce((s, c) => s + c.length, 0);
11859
- const out = new Uint8Array(total);
11860
- let offset = 0;
11861
- for (const c of chunks) {
11862
- out.set(c, offset);
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
- stdout: dec.decode(concat2(stdoutChunks)),
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
- async function listRaw() {
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: buildToolName(tool, needPrefix),
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 needPrefix = originPrefix(tools);
11958
- const tool = tools.find((t) => buildToolName(t, needPrefix) === name);
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(platform2, argv, stdinData, options.callTimeoutMs);
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: options.webmcpTab,
12842
+ tab,
12721
12843
  cdp,
12722
12844
  allowConsequential: options.webmcpAllowConsequential,
12723
12845
  ttlMs: options.webmcpTtl,
@@ -16274,6 +16396,15 @@ function collectHeaders(init) {
16274
16396
  }
16275
16397
  return entries;
16276
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
+ }
16277
16408
  function buildHttpRequest(method, url, headerEntries, bodyBytes) {
16278
16409
  const enc = new TextEncoder();
16279
16410
  const has = (n) => headerEntries.some(([k]) => k.toLowerCase() === n.toLowerCase());
@@ -16316,15 +16447,7 @@ function parseHttpResponse(buf, headerPos, bufLen) {
16316
16447
  }
16317
16448
  async function kernelLoopbackFetch(ctx, url, port, init, onRefused) {
16318
16449
  const method = (init?.method ?? "GET").toUpperCase();
16319
- const enc = new TextEncoder();
16320
16450
  const dec = new TextDecoder();
16321
- let bodyStr = "";
16322
- if (init?.body != null) {
16323
- bodyStr = typeof init.body === "string" ? init.body : dec.decode(init.body);
16324
- }
16325
- const bodyBytes = enc.encode(bodyStr);
16326
- const headerEntries = collectHeaders(init);
16327
- const reqBytes = buildHttpRequest(method, url, headerEntries, bodyBytes);
16328
16451
  let conn;
16329
16452
  try {
16330
16453
  conn = await ctx.net.connect({ host: "127.0.0.1", port });
@@ -16332,8 +16455,10 @@ async function kernelLoopbackFetch(ctx, url, port, init, onRefused) {
16332
16455
  if (onRefused && isConnectionRefused(err)) return onRefused();
16333
16456
  throw err;
16334
16457
  }
16458
+ const headerEntries = collectHeaders(init);
16459
+ const bodyBytes = await encodeBody(init?.body, headerEntries);
16335
16460
  const writer = conn.writable.getWriter();
16336
- await writer.write(reqBytes);
16461
+ await writer.write(buildHttpRequest(method, url, headerEntries, bodyBytes));
16337
16462
  const reader = conn.readable.getReader();
16338
16463
  let buf = new Uint8Array(4096);
16339
16464
  let bufLen = 0;
@@ -16452,7 +16577,7 @@ async function main(ctx) {
16452
16577
  packageDir: "/node_modules/@ai-ecoverse/mcpcat"
16453
16578
  });
16454
16579
  const args = ctx.argv.slice(1);
16455
- return run(platform2, "0.15.5", args);
16580
+ return run(platform2, "0.15.6", args);
16456
16581
  }
16457
16582
  export {
16458
16583
  main as default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/mcpcat",
3
- "version": "0.15.5",
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.91.0",
60
- "@ai-ecoverse/slicc-shared-web": "1.14.0",
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",