@ai-ecoverse/mcpcat 0.14.7 → 0.15.1

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.
@@ -8343,6 +8343,16 @@ var McpAuthError = class extends Error {
8343
8343
  this.target = target;
8344
8344
  }
8345
8345
  };
8346
+ var TransportRefusalError = class extends Error {
8347
+ status;
8348
+ reason;
8349
+ constructor(status, reason) {
8350
+ super(reason);
8351
+ this.name = "TransportRefusalError";
8352
+ this.status = status;
8353
+ this.reason = reason;
8354
+ }
8355
+ };
8346
8356
  var AuthRetryableError = class extends Error {
8347
8357
  response;
8348
8358
  constructor(response) {
@@ -8485,6 +8495,15 @@ async function createStatelessConnection(options) {
8485
8495
  return h;
8486
8496
  }
8487
8497
  async function handleErrorStatus(res, method) {
8498
+ if (res.status === 403) {
8499
+ const wwwAuth = res.headers.get("www-authenticate") ?? "";
8500
+ if (!wwwAuth) {
8501
+ const body = await res.text();
8502
+ if (body && !body.includes("insufficient_scope")) {
8503
+ throw new TransportRefusalError(403, body.split("\n")[0].slice(0, 200));
8504
+ }
8505
+ }
8506
+ }
8488
8507
  if ((res.status === 401 || res.status === 403) && authProvider)
8489
8508
  throw new AuthRetryableError(res);
8490
8509
  if (res.status === 401 || res.status === 403) throw new McpAuthError(res.status, url);
@@ -9018,6 +9037,7 @@ async function connect(platform2, options) {
9018
9037
  const isMethodNotFound = err instanceof StatelessRpcError && err.code === -32601 || err instanceof StatelessHttpError && err.status >= 400 && err.status < 500;
9019
9038
  if (options.protocolVersion?.startsWith("2026")) throw err;
9020
9039
  if (err instanceof McpAuthError || err instanceof AuthStepUpError) throw err;
9040
+ if (err instanceof TransportRefusalError) throw err;
9021
9041
  if (err.name === "McpOAuthAuthorizationRequiredError") throw err;
9022
9042
  if (isMethodNotFound) {
9023
9043
  return createPiConnection({
@@ -11803,7 +11823,8 @@ ${stderr}` : stderr : stdout;
11803
11823
  return {
11804
11824
  content: [{ type: "text", text }],
11805
11825
  isError: result.exitCode !== 0,
11806
- structuredContent: { stdout, stderr, exitCode: result.exitCode }
11826
+ structuredContent: { stdout, stderr, exitCode: result.exitCode },
11827
+ ...result.meta ? { _meta: result.meta } : {}
11807
11828
  };
11808
11829
  }
11809
11830
  function initializeResult(params, serverName, serverVersion) {
@@ -12103,6 +12124,219 @@ function createProxyHandler(options) {
12103
12124
  return { handle, shutdown };
12104
12125
  }
12105
12126
 
12127
+ // src/serve/webmcp.ts
12128
+ function buildBaseArgs(options) {
12129
+ const args2 = [];
12130
+ if (options.cdp) {
12131
+ args2.push("--cdp", options.cdp);
12132
+ }
12133
+ if (options.tab) {
12134
+ args2.push("--tab", options.tab);
12135
+ }
12136
+ return args2;
12137
+ }
12138
+ function generateNonce() {
12139
+ const bytes = new Uint8Array(8);
12140
+ crypto.getRandomValues(bytes);
12141
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
12142
+ }
12143
+ function wrapUntrusted(content) {
12144
+ const nonce = generateNonce();
12145
+ const openTag = `<<untrusted-page-content id=${nonce}>>`;
12146
+ const closeTag = `<<end id=${nonce}>>`;
12147
+ const neutralised = content.replaceAll("<<untrusted-page-content", "< <untrusted-page-content");
12148
+ return `${openTag}
12149
+ ${neutralised}
12150
+ ${closeTag}`;
12151
+ }
12152
+ async function runProcess(platform2, argv, stdin, timeoutMs) {
12153
+ let proc;
12154
+ try {
12155
+ proc = await platform2.spawn({ argv });
12156
+ } catch (err) {
12157
+ const code2 = err.code;
12158
+ if (code2 === "ENOENT" || code2 === "EACCES") {
12159
+ return { stdout: "", stderr: `command not found: ${argv[0]}`, exitCode: -1 };
12160
+ }
12161
+ throw err;
12162
+ }
12163
+ if (stdin !== void 0) {
12164
+ const writer = proc.stdin.getWriter();
12165
+ await writer.write(new TextEncoder().encode(stdin));
12166
+ await writer.close();
12167
+ } else {
12168
+ try {
12169
+ await proc.stdin.getWriter().close();
12170
+ } catch {
12171
+ }
12172
+ }
12173
+ const stdoutChunks = [];
12174
+ const stderrChunks = [];
12175
+ const readStream3 = async (stream, into) => {
12176
+ const r = stream.getReader();
12177
+ try {
12178
+ for (; ; ) {
12179
+ const { done, value } = await r.read();
12180
+ if (done) break;
12181
+ into.push(value);
12182
+ }
12183
+ } finally {
12184
+ r.releaseLock();
12185
+ }
12186
+ };
12187
+ const timer = new Promise(
12188
+ (resolve) => setTimeout(() => resolve("timeout"), timeoutMs ?? 3e4)
12189
+ );
12190
+ const work = Promise.all([
12191
+ readStream3(proc.stdout, stdoutChunks),
12192
+ readStream3(proc.stderr, stderrChunks),
12193
+ proc.wait()
12194
+ ]);
12195
+ const result = await Promise.race([work, timer]);
12196
+ if (result === "timeout") {
12197
+ await proc.kill("SIGTERM");
12198
+ return { stdout: "", stderr: "timed out", exitCode: 124 };
12199
+ }
12200
+ const [, , exitResult] = result;
12201
+ const dec = new TextDecoder();
12202
+ const concat2 = (chunks) => {
12203
+ const total = chunks.reduce((s, c) => s + c.length, 0);
12204
+ const out = new Uint8Array(total);
12205
+ let offset = 0;
12206
+ for (const c of chunks) {
12207
+ out.set(c, offset);
12208
+ offset += c.length;
12209
+ }
12210
+ return out;
12211
+ };
12212
+ return {
12213
+ stdout: dec.decode(concat2(stdoutChunks)),
12214
+ stderr: dec.decode(concat2(stderrChunks)),
12215
+ exitCode: exitResult.status
12216
+ };
12217
+ }
12218
+ function createWebMcpHandler(platform2, options) {
12219
+ let cache;
12220
+ const baseArgs = buildBaseArgs(options);
12221
+ async function listRaw() {
12222
+ const now = Date.now();
12223
+ if (cache && now < cache.expiresAt) {
12224
+ return { tools: cache.tools };
12225
+ }
12226
+ const argv = [options.playwrightCli, "webmcp-list", "--json", ...baseArgs];
12227
+ const result = await runProcess(platform2, argv, void 0, options.callTimeoutMs);
12228
+ if (result.exitCode === -1) {
12229
+ return { tools: [], error: result.stderr, notFound: true };
12230
+ }
12231
+ if (result.exitCode !== 0) {
12232
+ return { tools: [], error: result.stderr || `webmcp-list exited ${result.exitCode}` };
12233
+ }
12234
+ let parsed;
12235
+ try {
12236
+ parsed = JSON.parse(result.stdout);
12237
+ } catch {
12238
+ return { tools: [], error: "webmcp-list returned invalid JSON" };
12239
+ }
12240
+ if (!Array.isArray(parsed)) {
12241
+ return { tools: [], error: "webmcp-list did not return an array" };
12242
+ }
12243
+ const tools = parsed;
12244
+ cache = { tools, expiresAt: now + options.ttlMs };
12245
+ return { tools };
12246
+ }
12247
+ function mapDescriptionAnnotations(ann) {
12248
+ const mapped = { openWorldHint: true };
12249
+ if (!ann) return mapped;
12250
+ if (ann.readOnlyHint !== void 0) mapped.readOnlyHint = ann.readOnlyHint;
12251
+ if (ann.consequentialHint) mapped.destructiveHint = true;
12252
+ return mapped;
12253
+ }
12254
+ function originPrefix(tools) {
12255
+ const origins = new Set(tools.map((t) => t.origin));
12256
+ return origins.size > 1;
12257
+ }
12258
+ function hostFromOrigin(origin) {
12259
+ try {
12260
+ return new URL(origin).host;
12261
+ } catch {
12262
+ return origin;
12263
+ }
12264
+ }
12265
+ function buildDescription(tool) {
12266
+ const parts = [];
12267
+ if (tool.description) parts.push(tool.description);
12268
+ parts.push(`[origin: ${tool.origin}]`);
12269
+ return parts.join("\n");
12270
+ }
12271
+ function buildToolName(tool, prefix) {
12272
+ const base = tool.name;
12273
+ if (!prefix) return base;
12274
+ const host = hostFromOrigin(tool.origin).replace(/[^a-zA-Z0-9_]/g, "_");
12275
+ return `${host}__${base}`;
12276
+ }
12277
+ async function getRpcTools() {
12278
+ const { tools, error, notFound } = await listRaw();
12279
+ if (notFound) return null;
12280
+ if (error) return [];
12281
+ const needPrefix = originPrefix(tools);
12282
+ const rpcTools = [];
12283
+ for (const tool of tools) {
12284
+ if (tool.annotations?.consequentialHint && !options.allowConsequential) {
12285
+ continue;
12286
+ }
12287
+ const mapped = mapDescriptionAnnotations(tool.annotations);
12288
+ rpcTools.push({
12289
+ name: buildToolName(tool, needPrefix),
12290
+ description: buildDescription(tool),
12291
+ inputSchema: tool.inputSchema,
12292
+ annotations: mapped
12293
+ });
12294
+ }
12295
+ return rpcTools;
12296
+ }
12297
+ async function callTool(name, args2) {
12298
+ const { tools, error } = await listRaw();
12299
+ if (error) {
12300
+ return { stdout: "", stderr: error, exitCode: 2 };
12301
+ }
12302
+ const needPrefix = originPrefix(tools);
12303
+ const tool = tools.find((t) => buildToolName(t, needPrefix) === name);
12304
+ if (!tool) {
12305
+ return { stdout: "", stderr: `unknown tool: ${name}`, exitCode: 2 };
12306
+ }
12307
+ const argv = [options.playwrightCli, "webmcp-call", tool.name, "-", ...baseArgs];
12308
+ if (options.allowConsequential) {
12309
+ argv.push("--allow-consequential");
12310
+ }
12311
+ const stdinData = JSON.stringify(args2 ?? {});
12312
+ const result = await runProcess(platform2, argv, stdinData, options.callTimeoutMs);
12313
+ if (result.exitCode === -1) {
12314
+ throw new Error(result.stderr || "playwright-cli not found");
12315
+ }
12316
+ if (result.exitCode === 2) {
12317
+ throw new Error(result.stderr || "webmcp-call error");
12318
+ }
12319
+ const meta = {
12320
+ "io.mcpcat/untrusted": true,
12321
+ origin: tool.origin
12322
+ };
12323
+ if (result.exitCode === 1) {
12324
+ return {
12325
+ stdout: "",
12326
+ stderr: wrapUntrusted(result.stderr || result.stdout),
12327
+ exitCode: 1,
12328
+ meta
12329
+ };
12330
+ }
12331
+ const text = wrapUntrusted(result.stdout);
12332
+ return { stdout: text, stderr: result.stderr, exitCode: 0, meta };
12333
+ }
12334
+ function invalidateCache() {
12335
+ cache = void 0;
12336
+ }
12337
+ return { getRpcTools, callTool, invalidateCache };
12338
+ }
12339
+
12106
12340
  // src/serve/runtime.ts
12107
12341
  var CALL_TIMEOUT_MS = 12e4;
12108
12342
  var MAX_OUTPUT_DEFAULT = 1048576;
@@ -12137,6 +12371,10 @@ function parseSingleFlag(args2, i, opts) {
12137
12371
  opts.json = true;
12138
12372
  } else if (arg === "--no-auth") {
12139
12373
  opts.noAuth = true;
12374
+ } else if (arg === "--webmcp") {
12375
+ opts.webmcp = true;
12376
+ } else if (arg === "--allow-consequential") {
12377
+ opts.webmcpAllowConsequential = true;
12140
12378
  } else if (!arg.startsWith("-")) {
12141
12379
  opts.commands.push(parseCommandTarget(arg));
12142
12380
  } else {
@@ -12178,6 +12416,18 @@ function parseValueFlag(args2, i, opts) {
12178
12416
  case "--cwd":
12179
12417
  opts.cwd = args2[++i];
12180
12418
  break;
12419
+ case "--tab":
12420
+ opts.webmcpTab = args2[++i];
12421
+ break;
12422
+ case "--webmcp-ttl":
12423
+ opts.webmcpTtl = Number(args2[++i]) * 1e3;
12424
+ break;
12425
+ case "--playwright-cli":
12426
+ opts.webmcpPlaywrightCli = args2[++i];
12427
+ break;
12428
+ case "--cdp":
12429
+ opts.webmcpCdp = args2[++i];
12430
+ break;
12181
12431
  case "-H":
12182
12432
  case "--header": {
12183
12433
  const hdr = args2[++i];
@@ -12210,7 +12460,13 @@ function parseServeFlags(args2) {
12210
12460
  json: false,
12211
12461
  commands: [],
12212
12462
  proxy: void 0,
12213
- proxyHeaders: []
12463
+ proxyHeaders: [],
12464
+ webmcp: false,
12465
+ webmcpTab: void 0,
12466
+ webmcpTtl: 5e3,
12467
+ webmcpAllowConsequential: false,
12468
+ webmcpPlaywrightCli: void 0,
12469
+ webmcpCdp: void 0
12214
12470
  };
12215
12471
  for (let i = 0; i < args2.length; i++) {
12216
12472
  i = parseSingleFlag(args2, i, opts);
@@ -12592,25 +12848,25 @@ async function discoverWithSignal(platform2, commands, childEnv, cwd) {
12592
12848
  if (earlyShutdown) return { exitCode: 143, inflightDone };
12593
12849
  return { clis };
12594
12850
  }
12595
- async function runServe(platform2, options, version) {
12596
- let resolvedBearer;
12597
- let generatedBearer = false;
12851
+ async function resolveBearer(platform2, options) {
12598
12852
  if (options._resolvedBearer) {
12599
- resolvedBearer = options._resolvedBearer;
12600
- } else if (options.bearer) {
12853
+ return { resolved: options._resolvedBearer, generated: false };
12854
+ }
12855
+ if (options.bearer) {
12601
12856
  const expanded = expandBearerToken(options.bearer, platform2.env);
12602
12857
  if (!expanded) {
12603
12858
  await platform2.write(
12604
12859
  2,
12605
12860
  "mcpcat serve: --bearer requires a ${NAME} variable reference, not a literal token. Use --bearer '${MY_TOKEN}' and set MY_TOKEN in the environment.\n"
12606
12861
  );
12607
- return 2;
12862
+ return { exitCode: 2 };
12608
12863
  }
12609
- resolvedBearer = expanded;
12610
- } else if (options.noAuth) {
12864
+ return { resolved: expanded, generated: false };
12865
+ }
12866
+ if (options.noAuth) {
12611
12867
  if (options.transport === "http" && !isLoopbackBind(options.host)) {
12612
12868
  await platform2.write(2, "mcpcat serve: --no-auth is only allowed on loopback\n");
12613
- return 2;
12869
+ return { exitCode: 2 };
12614
12870
  }
12615
12871
  if (options.transport === "http") {
12616
12872
  await platform2.write(
@@ -12618,21 +12874,30 @@ async function runServe(platform2, options, version) {
12618
12874
  "mcpcat serve: warning: running without authentication on loopback\n"
12619
12875
  );
12620
12876
  }
12621
- resolvedBearer = void 0;
12622
- } else if (options.transport === "http") {
12877
+ return { resolved: void 0, generated: false };
12878
+ }
12879
+ if (options.transport === "http") {
12623
12880
  if (platform2.kind === "kernel") {
12624
12881
  await platform2.write(
12625
12882
  2,
12626
12883
  "mcpcat serve: kernel HTTP serve requires --bearer '${VAR}' or --no-auth\n"
12627
12884
  );
12628
- return 2;
12885
+ return { exitCode: 2 };
12629
12886
  }
12630
- resolvedBearer = generateToken();
12631
- generatedBearer = true;
12887
+ return { resolved: generateToken(), generated: true };
12632
12888
  }
12889
+ return { resolved: void 0, generated: false };
12890
+ }
12891
+ async function runServe(platform2, options, version) {
12892
+ const bearerResult = await resolveBearer(platform2, options);
12893
+ if ("exitCode" in bearerResult) return bearerResult.exitCode;
12894
+ const { resolved: resolvedBearer, generated: generatedBearer } = bearerResult;
12633
12895
  if (options.proxy) {
12634
12896
  return runProxyServe(platform2, options, version, resolvedBearer, generatedBearer);
12635
12897
  }
12898
+ if (options.webmcp) {
12899
+ return runWebMcpServe(platform2, options, version, resolvedBearer, generatedBearer);
12900
+ }
12636
12901
  const serveCwd = options.cwd ?? platform2.cwd;
12637
12902
  const childEnv = buildChildEnv(platform2, options.envAllow);
12638
12903
  const discovery = await discoverWithSignal(platform2, options.commands, childEnv, serveCwd);
@@ -12766,6 +13031,233 @@ async function buildProxyConnectOpts(platform2, options, version) {
12766
13031
  }
12767
13032
  return { connectOpts, hiddenValues };
12768
13033
  }
13034
+ function resolvePlaywrightCli(platform2, explicit) {
13035
+ if (explicit) return explicit;
13036
+ if (platform2.kind === "kernel") return "playwright-cli";
13037
+ const pathEnv = platform2.env.PATH ?? "";
13038
+ const sep = pathEnv.includes(";") ? ";" : ":";
13039
+ for (const dir of pathEnv.split(sep)) {
13040
+ if (!dir) continue;
13041
+ return "playwright-cli";
13042
+ }
13043
+ return void 0;
13044
+ }
13045
+ async function runWebMcpServe(platform2, options, version, resolvedBearer, generatedBearer) {
13046
+ const cliPath = resolvePlaywrightCli(platform2, options.webmcpPlaywrightCli);
13047
+ if (!cliPath) {
13048
+ await platform2.write(
13049
+ 2,
13050
+ "mcpcat serve --webmcp: playwright-cli not found. Install it with: curl -fsSL https://get.sliccy.ai/playwright-cli | sh\n"
13051
+ );
13052
+ return 3;
13053
+ }
13054
+ const cdp = options.webmcpCdp ?? platform2.env.SLICC_CDP_URL;
13055
+ const handler = createWebMcpHandler(platform2, {
13056
+ playwrightCli: cliPath,
13057
+ tab: options.webmcpTab,
13058
+ cdp,
13059
+ allowConsequential: options.webmcpAllowConsequential,
13060
+ ttlMs: options.webmcpTtl,
13061
+ callTimeoutMs: options.callTimeout,
13062
+ maxOutput: options.maxOutput
13063
+ });
13064
+ const probe = await handler.getRpcTools();
13065
+ if (probe === null) {
13066
+ await platform2.write(
13067
+ 2,
13068
+ "mcpcat serve --webmcp: playwright-cli not found. Install it with: curl -fsSL https://get.sliccy.ai/playwright-cli | sh\n"
13069
+ );
13070
+ return 3;
13071
+ }
13072
+ const serverName = "mcpcat-webmcp";
13073
+ const getTools = async () => await handler.getRpcTools() ?? [];
13074
+ const callTool = (name, args2) => handler.callTool(name, args2);
13075
+ if (options.transport === "stdio") {
13076
+ return runWebMcpStdio(platform2, getTools, callTool, serverName, version);
13077
+ }
13078
+ return runWebMcpHttp(
13079
+ platform2,
13080
+ options,
13081
+ getTools,
13082
+ callTool,
13083
+ serverName,
13084
+ version,
13085
+ resolvedBearer,
13086
+ generatedBearer
13087
+ );
13088
+ }
13089
+ async function runWebMcpStdio(platform2, getTools, callTool, serverName, version) {
13090
+ const reader = platform2.stdin.getReader();
13091
+ let buffer = "";
13092
+ const dec = new TextDecoder();
13093
+ const enc = new TextEncoder();
13094
+ try {
13095
+ for (; ; ) {
13096
+ const { done, value } = await reader.read();
13097
+ if (done) break;
13098
+ buffer += dec.decode(value, { stream: true });
13099
+ let nlIdx;
13100
+ while ((nlIdx = buffer.indexOf("\n")) >= 0) {
13101
+ const line = buffer.slice(0, nlIdx).trim();
13102
+ buffer = buffer.slice(nlIdx + 1);
13103
+ if (!line) continue;
13104
+ const tools = await getTools();
13105
+ const outcome = await handleJsonRpc(line, tools, callTool, serverName, version);
13106
+ if (outcome.status === 202) continue;
13107
+ const writer = platform2.stdout.getWriter();
13108
+ await writer.write(enc.encode(`${outcome.body}
13109
+ `));
13110
+ writer.releaseLock();
13111
+ }
13112
+ }
13113
+ } finally {
13114
+ reader.releaseLock();
13115
+ }
13116
+ return 0;
13117
+ }
13118
+ async function runWebMcpHttp(platform2, options, getTools, callTool, serverName, version, resolvedBearer, generatedBearer) {
13119
+ if (!platform2.listen) {
13120
+ await platform2.write(2, "mcpcat serve --webmcp: HTTP transport requires platform.listen\n");
13121
+ return 3;
13122
+ }
13123
+ const boundLoopback = isLoopbackBind(options.host);
13124
+ if (!boundLoopback && !resolvedBearer) {
13125
+ await platform2.write(
13126
+ 2,
13127
+ "mcpcat serve --webmcp: --bearer is required when binding off loopback\n"
13128
+ );
13129
+ return 2;
13130
+ }
13131
+ const allowedOrigins = new Set(options.allowOrigins);
13132
+ const listener = await platform2.listen({ host: options.host, port: options.port });
13133
+ const actualPort = listener.port;
13134
+ const serverOrigins = buildServerOrigins(options.host, actualPort);
13135
+ const allowedHosts = buildAllowedHosts(options.host, actualPort, options.allowHosts);
13136
+ const listenUrl = `http://${options.host}:${actualPort}/mcp`;
13137
+ let tokenFilePath;
13138
+ if (generatedBearer && resolvedBearer) {
13139
+ tokenFilePath = await publishBearer(
13140
+ platform2,
13141
+ resolvedBearer,
13142
+ actualPort,
13143
+ listenUrl,
13144
+ options.json
13145
+ );
13146
+ } else {
13147
+ if (options.json) {
13148
+ await platform2.write(1, `${JSON.stringify({ url: listenUrl })}
13149
+ `);
13150
+ }
13151
+ }
13152
+ await platform2.write(2, `mcpcat serve --webmcp: listening on ${listenUrl}
13153
+ `);
13154
+ if (options.onListening) {
13155
+ await options.onListening(actualPort);
13156
+ }
13157
+ let shutdownRequested = false;
13158
+ const shutdown = () => {
13159
+ if (shutdownRequested) return;
13160
+ shutdownRequested = true;
13161
+ listener.close();
13162
+ if (tokenFilePath) {
13163
+ platform2.fs.unlink?.(tokenFilePath).catch(() => {
13164
+ });
13165
+ }
13166
+ };
13167
+ if (platform2.onSignal) {
13168
+ platform2.onSignal("SIGINT", shutdown);
13169
+ platform2.onSignal("SIGTERM", shutdown);
13170
+ platform2.onSignal("SIGHUP", shutdown);
13171
+ }
13172
+ const handler = async (request) => {
13173
+ if (request.target !== "/mcp") {
13174
+ return {
13175
+ status: 404,
13176
+ reason: "Not Found",
13177
+ headers: { "content-type": "text/plain" },
13178
+ body: "Not Found"
13179
+ };
13180
+ }
13181
+ if (request.method !== "POST") {
13182
+ return {
13183
+ status: 405,
13184
+ reason: "Method Not Allowed",
13185
+ headers: { "content-type": "text/plain", allow: "POST" },
13186
+ body: "Method Not Allowed"
13187
+ };
13188
+ }
13189
+ if (!checkContentType(request.headers["content-type"])) {
13190
+ return {
13191
+ status: 415,
13192
+ reason: "Unsupported Media Type",
13193
+ headers: { "content-type": "text/plain" },
13194
+ body: "Content-Type must be application/json"
13195
+ };
13196
+ }
13197
+ if (!checkAcceptHeader(request.headers.accept)) {
13198
+ return {
13199
+ status: 406,
13200
+ reason: "Not Acceptable",
13201
+ headers: { "content-type": "text/plain" },
13202
+ body: "Accept must include application/json or text/event-stream"
13203
+ };
13204
+ }
13205
+ const originCheck = checkOrigin(request.headers.origin, serverOrigins, allowedOrigins);
13206
+ if (!originCheck.ok) {
13207
+ return {
13208
+ status: 403,
13209
+ reason: "Forbidden",
13210
+ headers: { "content-type": "text/plain" },
13211
+ body: originCheck.reason ?? "Forbidden"
13212
+ };
13213
+ }
13214
+ const hostCheck = checkHost(request.headers.host, allowedHosts);
13215
+ if (!hostCheck.ok) {
13216
+ return {
13217
+ status: 403,
13218
+ reason: "Forbidden",
13219
+ headers: { "content-type": "text/plain" },
13220
+ body: hostCheck.reason ?? "Forbidden"
13221
+ };
13222
+ }
13223
+ if (resolvedBearer) {
13224
+ const authHeader = request.headers.authorization;
13225
+ const provided = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : "";
13226
+ if (!provided || !constantTimeEqual(provided, resolvedBearer)) {
13227
+ return {
13228
+ status: 401,
13229
+ reason: "Unauthorized",
13230
+ headers: { "content-type": "text/plain" },
13231
+ body: "Unauthorized"
13232
+ };
13233
+ }
13234
+ }
13235
+ const tools = await getTools();
13236
+ const outcome = await handleJsonRpc(request.body, tools, callTool, serverName, version);
13237
+ return {
13238
+ status: outcome.status,
13239
+ reason: outcome.status === 200 ? "OK" : "Accepted",
13240
+ headers: { "content-type": outcome.contentType },
13241
+ body: outcome.body
13242
+ };
13243
+ };
13244
+ try {
13245
+ for await (const connection of listener) {
13246
+ if (shutdownRequested) break;
13247
+ const conn = connection;
13248
+ handleConnection(conn.readable, conn.writable, handler).catch(() => {
13249
+ }).finally(() => {
13250
+ try {
13251
+ conn.close();
13252
+ } catch {
13253
+ }
13254
+ });
13255
+ }
13256
+ } catch {
13257
+ if (!shutdownRequested) throw new Error("listener closed unexpectedly");
13258
+ }
13259
+ return 0;
13260
+ }
12769
13261
  async function runProxyServe(platform2, options, version, resolvedBearer, generatedBearer) {
12770
13262
  const built = await buildProxyConnectOpts(platform2, options, version);
12771
13263
  if ("error" in built) {
@@ -13977,7 +14469,10 @@ async function sealSecret(platform2, name, value, options) {
13977
14469
  }
13978
14470
  const response = JSON.parse(responseText);
13979
14471
  if (response.error) {
13980
- throw Object.assign(new Error(response.error), { sealErrno: response.errno ?? "EACCES" });
14472
+ const isStructured = typeof response.error === "object" && response.error !== null && response.error.code;
14473
+ const errno = isStructured ? response.error.code : response.errno ?? "EACCES";
14474
+ const message = isStructured ? response.error.message : response.error;
14475
+ throw Object.assign(new Error(String(message)), { sealErrno: errno });
13981
14476
  }
13982
14477
  return { placeholder: response.placeholder };
13983
14478
  }
@@ -13989,6 +14484,7 @@ function sealErrorMessage(errno, name) {
13989
14484
  if (errno === "EEXIST") return `a non-bridge secret named ${name} already exists`;
13990
14485
  if (errno === "ENOTCONN") return "no vault available; use --bearer '${VAR}' or --no-auth";
13991
14486
  if (errno === "EACCES") return "vault locked; use --bearer '${VAR}' or --no-auth";
14487
+ if (errno === "ETIMEDOUT") return "vault did not respond in time";
13992
14488
  return `vault error: ${errno}`;
13993
14489
  }
13994
14490
 
@@ -15344,7 +15840,7 @@ async function dispatchDiscoveryVerb(platform2, parsed, args2) {
15344
15840
  async function dispatchServeVerb(platform2, args2, version) {
15345
15841
  const serveArgs = args2.slice(args2.indexOf("serve") + 1);
15346
15842
  const serveOptions = parseServeFlags(serveArgs);
15347
- if (!serveOptions.proxy && serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
15843
+ if (!serveOptions.proxy && !serveOptions.webmcp && serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
15348
15844
  await platform2.write(2, "mcpcat serve: no commands or --tool templates given\n");
15349
15845
  return 2;
15350
15846
  }
@@ -15616,6 +16112,9 @@ function classifyError(err) {
15616
16112
  if (err instanceof InputRequiredError) {
15617
16113
  return { code: EXIT_INPUT_REQUIRED, kind: "input_required", msg: err.requestJson };
15618
16114
  }
16115
+ if (err instanceof TransportRefusalError) {
16116
+ return { code: EXIT_CONNECTION, kind: "connection", msg: err.reason };
16117
+ }
15619
16118
  if (err instanceof McpTimeoutError2) {
15620
16119
  return { code: EXIT_TIMEOUT, kind: "timeout", msg: err.message };
15621
16120
  }
@@ -15672,6 +16171,14 @@ async function resolveAuthProvider(platform2, hasAuthHeader, url, target, config
15672
16171
  return { provider, credentialSecrets };
15673
16172
  }
15674
16173
  function classifyConnectionError(err, target, interrupted) {
16174
+ if (err instanceof TransportRefusalError) {
16175
+ const hint = "in seven, reach the host via http://host.slicc.internal:<port>; in tests, enable the transport's hostLoopback";
16176
+ return {
16177
+ code: EXIT_CONNECTION,
16178
+ kind: "connection",
16179
+ msg: `${err.reason} (${hint})`
16180
+ };
16181
+ }
15675
16182
  if (err instanceof McpAuthError || err instanceof AuthStepUpError || err instanceof JerkSignInRequiredError) {
15676
16183
  return classifyError(err);
15677
16184
  }
@@ -16177,5 +16684,5 @@ try {
16177
16684
  });
16178
16685
  } catch {
16179
16686
  }
16180
- var code = await run(platform, "0.14.7", args);
16687
+ var code = await run(platform, "0.15.1", args);
16181
16688
  process.exit(code);
@@ -7964,6 +7964,16 @@ var McpAuthError = class extends Error {
7964
7964
  this.target = target;
7965
7965
  }
7966
7966
  };
7967
+ var TransportRefusalError = class extends Error {
7968
+ status;
7969
+ reason;
7970
+ constructor(status, reason) {
7971
+ super(reason);
7972
+ this.name = "TransportRefusalError";
7973
+ this.status = status;
7974
+ this.reason = reason;
7975
+ }
7976
+ };
7967
7977
  var AuthRetryableError = class extends Error {
7968
7978
  response;
7969
7979
  constructor(response) {
@@ -8106,6 +8116,15 @@ async function createStatelessConnection(options) {
8106
8116
  return h;
8107
8117
  }
8108
8118
  async function handleErrorStatus(res, method) {
8119
+ if (res.status === 403) {
8120
+ const wwwAuth = res.headers.get("www-authenticate") ?? "";
8121
+ if (!wwwAuth) {
8122
+ const body = await res.text();
8123
+ if (body && !body.includes("insufficient_scope")) {
8124
+ throw new TransportRefusalError(403, body.split("\n")[0].slice(0, 200));
8125
+ }
8126
+ }
8127
+ }
8109
8128
  if ((res.status === 401 || res.status === 403) && authProvider)
8110
8129
  throw new AuthRetryableError(res);
8111
8130
  if (res.status === 401 || res.status === 403) throw new McpAuthError(res.status, url);
@@ -8639,6 +8658,7 @@ async function connect(platform2, options) {
8639
8658
  const isMethodNotFound = err instanceof StatelessRpcError && err.code === -32601 || err instanceof StatelessHttpError && err.status >= 400 && err.status < 500;
8640
8659
  if (options.protocolVersion?.startsWith("2026")) throw err;
8641
8660
  if (err instanceof McpAuthError || err instanceof AuthStepUpError) throw err;
8661
+ if (err instanceof TransportRefusalError) throw err;
8642
8662
  if (err.name === "McpOAuthAuthorizationRequiredError") throw err;
8643
8663
  if (isMethodNotFound) {
8644
8664
  return createPiConnection({
@@ -11424,7 +11444,8 @@ ${stderr}` : stderr : stdout;
11424
11444
  return {
11425
11445
  content: [{ type: "text", text }],
11426
11446
  isError: result.exitCode !== 0,
11427
- structuredContent: { stdout, stderr, exitCode: result.exitCode }
11447
+ structuredContent: { stdout, stderr, exitCode: result.exitCode },
11448
+ ...result.meta ? { _meta: result.meta } : {}
11428
11449
  };
11429
11450
  }
11430
11451
  function initializeResult(params, serverName, serverVersion) {
@@ -11724,6 +11745,219 @@ function createProxyHandler(options) {
11724
11745
  return { handle, shutdown };
11725
11746
  }
11726
11747
 
11748
+ // src/serve/webmcp.ts
11749
+ function buildBaseArgs(options) {
11750
+ const args = [];
11751
+ if (options.cdp) {
11752
+ args.push("--cdp", options.cdp);
11753
+ }
11754
+ if (options.tab) {
11755
+ args.push("--tab", options.tab);
11756
+ }
11757
+ return args;
11758
+ }
11759
+ function generateNonce() {
11760
+ const bytes = new Uint8Array(8);
11761
+ crypto.getRandomValues(bytes);
11762
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
11763
+ }
11764
+ function wrapUntrusted(content) {
11765
+ const nonce = generateNonce();
11766
+ const openTag = `<<untrusted-page-content id=${nonce}>>`;
11767
+ const closeTag = `<<end id=${nonce}>>`;
11768
+ const neutralised = content.replaceAll("<<untrusted-page-content", "< <untrusted-page-content");
11769
+ return `${openTag}
11770
+ ${neutralised}
11771
+ ${closeTag}`;
11772
+ }
11773
+ async function runProcess(platform2, argv, stdin, timeoutMs) {
11774
+ let proc;
11775
+ try {
11776
+ proc = await platform2.spawn({ argv });
11777
+ } catch (err) {
11778
+ const code = err.code;
11779
+ if (code === "ENOENT" || code === "EACCES") {
11780
+ return { stdout: "", stderr: `command not found: ${argv[0]}`, exitCode: -1 };
11781
+ }
11782
+ throw err;
11783
+ }
11784
+ if (stdin !== void 0) {
11785
+ const writer = proc.stdin.getWriter();
11786
+ await writer.write(new TextEncoder().encode(stdin));
11787
+ await writer.close();
11788
+ } else {
11789
+ try {
11790
+ await proc.stdin.getWriter().close();
11791
+ } catch {
11792
+ }
11793
+ }
11794
+ const stdoutChunks = [];
11795
+ const stderrChunks = [];
11796
+ const readStream3 = async (stream, into) => {
11797
+ const r = stream.getReader();
11798
+ try {
11799
+ for (; ; ) {
11800
+ const { done, value } = await r.read();
11801
+ if (done) break;
11802
+ into.push(value);
11803
+ }
11804
+ } finally {
11805
+ r.releaseLock();
11806
+ }
11807
+ };
11808
+ const timer = new Promise(
11809
+ (resolve2) => setTimeout(() => resolve2("timeout"), timeoutMs ?? 3e4)
11810
+ );
11811
+ const work = Promise.all([
11812
+ readStream3(proc.stdout, stdoutChunks),
11813
+ readStream3(proc.stderr, stderrChunks),
11814
+ proc.wait()
11815
+ ]);
11816
+ const result = await Promise.race([work, timer]);
11817
+ if (result === "timeout") {
11818
+ await proc.kill("SIGTERM");
11819
+ return { stdout: "", stderr: "timed out", exitCode: 124 };
11820
+ }
11821
+ const [, , exitResult] = result;
11822
+ const dec = new TextDecoder();
11823
+ const concat2 = (chunks) => {
11824
+ const total = chunks.reduce((s, c) => s + c.length, 0);
11825
+ const out = new Uint8Array(total);
11826
+ let offset = 0;
11827
+ for (const c of chunks) {
11828
+ out.set(c, offset);
11829
+ offset += c.length;
11830
+ }
11831
+ return out;
11832
+ };
11833
+ return {
11834
+ stdout: dec.decode(concat2(stdoutChunks)),
11835
+ stderr: dec.decode(concat2(stderrChunks)),
11836
+ exitCode: exitResult.status
11837
+ };
11838
+ }
11839
+ function createWebMcpHandler(platform2, options) {
11840
+ let cache;
11841
+ const baseArgs = buildBaseArgs(options);
11842
+ async function listRaw() {
11843
+ const now = Date.now();
11844
+ if (cache && now < cache.expiresAt) {
11845
+ return { tools: cache.tools };
11846
+ }
11847
+ const argv = [options.playwrightCli, "webmcp-list", "--json", ...baseArgs];
11848
+ const result = await runProcess(platform2, argv, void 0, options.callTimeoutMs);
11849
+ if (result.exitCode === -1) {
11850
+ return { tools: [], error: result.stderr, notFound: true };
11851
+ }
11852
+ if (result.exitCode !== 0) {
11853
+ return { tools: [], error: result.stderr || `webmcp-list exited ${result.exitCode}` };
11854
+ }
11855
+ let parsed;
11856
+ try {
11857
+ parsed = JSON.parse(result.stdout);
11858
+ } catch {
11859
+ return { tools: [], error: "webmcp-list returned invalid JSON" };
11860
+ }
11861
+ if (!Array.isArray(parsed)) {
11862
+ return { tools: [], error: "webmcp-list did not return an array" };
11863
+ }
11864
+ const tools = parsed;
11865
+ cache = { tools, expiresAt: now + options.ttlMs };
11866
+ return { tools };
11867
+ }
11868
+ function mapDescriptionAnnotations(ann) {
11869
+ const mapped = { openWorldHint: true };
11870
+ if (!ann) return mapped;
11871
+ if (ann.readOnlyHint !== void 0) mapped.readOnlyHint = ann.readOnlyHint;
11872
+ if (ann.consequentialHint) mapped.destructiveHint = true;
11873
+ return mapped;
11874
+ }
11875
+ function originPrefix(tools) {
11876
+ const origins = new Set(tools.map((t) => t.origin));
11877
+ return origins.size > 1;
11878
+ }
11879
+ function hostFromOrigin(origin) {
11880
+ try {
11881
+ return new URL(origin).host;
11882
+ } catch {
11883
+ return origin;
11884
+ }
11885
+ }
11886
+ function buildDescription(tool) {
11887
+ const parts = [];
11888
+ if (tool.description) parts.push(tool.description);
11889
+ parts.push(`[origin: ${tool.origin}]`);
11890
+ return parts.join("\n");
11891
+ }
11892
+ function buildToolName(tool, prefix) {
11893
+ const base = tool.name;
11894
+ if (!prefix) return base;
11895
+ const host = hostFromOrigin(tool.origin).replace(/[^a-zA-Z0-9_]/g, "_");
11896
+ return `${host}__${base}`;
11897
+ }
11898
+ async function getRpcTools() {
11899
+ const { tools, error, notFound } = await listRaw();
11900
+ if (notFound) return null;
11901
+ if (error) return [];
11902
+ const needPrefix = originPrefix(tools);
11903
+ const rpcTools = [];
11904
+ for (const tool of tools) {
11905
+ if (tool.annotations?.consequentialHint && !options.allowConsequential) {
11906
+ continue;
11907
+ }
11908
+ const mapped = mapDescriptionAnnotations(tool.annotations);
11909
+ rpcTools.push({
11910
+ name: buildToolName(tool, needPrefix),
11911
+ description: buildDescription(tool),
11912
+ inputSchema: tool.inputSchema,
11913
+ annotations: mapped
11914
+ });
11915
+ }
11916
+ return rpcTools;
11917
+ }
11918
+ async function callTool(name, args) {
11919
+ const { tools, error } = await listRaw();
11920
+ if (error) {
11921
+ return { stdout: "", stderr: error, exitCode: 2 };
11922
+ }
11923
+ const needPrefix = originPrefix(tools);
11924
+ const tool = tools.find((t) => buildToolName(t, needPrefix) === name);
11925
+ if (!tool) {
11926
+ return { stdout: "", stderr: `unknown tool: ${name}`, exitCode: 2 };
11927
+ }
11928
+ const argv = [options.playwrightCli, "webmcp-call", tool.name, "-", ...baseArgs];
11929
+ if (options.allowConsequential) {
11930
+ argv.push("--allow-consequential");
11931
+ }
11932
+ const stdinData = JSON.stringify(args ?? {});
11933
+ const result = await runProcess(platform2, argv, stdinData, options.callTimeoutMs);
11934
+ if (result.exitCode === -1) {
11935
+ throw new Error(result.stderr || "playwright-cli not found");
11936
+ }
11937
+ if (result.exitCode === 2) {
11938
+ throw new Error(result.stderr || "webmcp-call error");
11939
+ }
11940
+ const meta = {
11941
+ "io.mcpcat/untrusted": true,
11942
+ origin: tool.origin
11943
+ };
11944
+ if (result.exitCode === 1) {
11945
+ return {
11946
+ stdout: "",
11947
+ stderr: wrapUntrusted(result.stderr || result.stdout),
11948
+ exitCode: 1,
11949
+ meta
11950
+ };
11951
+ }
11952
+ const text = wrapUntrusted(result.stdout);
11953
+ return { stdout: text, stderr: result.stderr, exitCode: 0, meta };
11954
+ }
11955
+ function invalidateCache() {
11956
+ cache = void 0;
11957
+ }
11958
+ return { getRpcTools, callTool, invalidateCache };
11959
+ }
11960
+
11727
11961
  // src/serve/runtime.ts
11728
11962
  var CALL_TIMEOUT_MS = 12e4;
11729
11963
  var MAX_OUTPUT_DEFAULT = 1048576;
@@ -11758,6 +11992,10 @@ function parseSingleFlag(args, i, opts) {
11758
11992
  opts.json = true;
11759
11993
  } else if (arg === "--no-auth") {
11760
11994
  opts.noAuth = true;
11995
+ } else if (arg === "--webmcp") {
11996
+ opts.webmcp = true;
11997
+ } else if (arg === "--allow-consequential") {
11998
+ opts.webmcpAllowConsequential = true;
11761
11999
  } else if (!arg.startsWith("-")) {
11762
12000
  opts.commands.push(parseCommandTarget(arg));
11763
12001
  } else {
@@ -11799,6 +12037,18 @@ function parseValueFlag(args, i, opts) {
11799
12037
  case "--cwd":
11800
12038
  opts.cwd = args[++i];
11801
12039
  break;
12040
+ case "--tab":
12041
+ opts.webmcpTab = args[++i];
12042
+ break;
12043
+ case "--webmcp-ttl":
12044
+ opts.webmcpTtl = Number(args[++i]) * 1e3;
12045
+ break;
12046
+ case "--playwright-cli":
12047
+ opts.webmcpPlaywrightCli = args[++i];
12048
+ break;
12049
+ case "--cdp":
12050
+ opts.webmcpCdp = args[++i];
12051
+ break;
11802
12052
  case "-H":
11803
12053
  case "--header": {
11804
12054
  const hdr = args[++i];
@@ -11831,7 +12081,13 @@ function parseServeFlags(args) {
11831
12081
  json: false,
11832
12082
  commands: [],
11833
12083
  proxy: void 0,
11834
- proxyHeaders: []
12084
+ proxyHeaders: [],
12085
+ webmcp: false,
12086
+ webmcpTab: void 0,
12087
+ webmcpTtl: 5e3,
12088
+ webmcpAllowConsequential: false,
12089
+ webmcpPlaywrightCli: void 0,
12090
+ webmcpCdp: void 0
11835
12091
  };
11836
12092
  for (let i = 0; i < args.length; i++) {
11837
12093
  i = parseSingleFlag(args, i, opts);
@@ -12213,25 +12469,25 @@ async function discoverWithSignal(platform2, commands, childEnv, cwd) {
12213
12469
  if (earlyShutdown) return { exitCode: 143, inflightDone };
12214
12470
  return { clis };
12215
12471
  }
12216
- async function runServe(platform2, options, version) {
12217
- let resolvedBearer;
12218
- let generatedBearer = false;
12472
+ async function resolveBearer(platform2, options) {
12219
12473
  if (options._resolvedBearer) {
12220
- resolvedBearer = options._resolvedBearer;
12221
- } else if (options.bearer) {
12474
+ return { resolved: options._resolvedBearer, generated: false };
12475
+ }
12476
+ if (options.bearer) {
12222
12477
  const expanded = expandBearerToken(options.bearer, platform2.env);
12223
12478
  if (!expanded) {
12224
12479
  await platform2.write(
12225
12480
  2,
12226
12481
  "mcpcat serve: --bearer requires a ${NAME} variable reference, not a literal token. Use --bearer '${MY_TOKEN}' and set MY_TOKEN in the environment.\n"
12227
12482
  );
12228
- return 2;
12483
+ return { exitCode: 2 };
12229
12484
  }
12230
- resolvedBearer = expanded;
12231
- } else if (options.noAuth) {
12485
+ return { resolved: expanded, generated: false };
12486
+ }
12487
+ if (options.noAuth) {
12232
12488
  if (options.transport === "http" && !isLoopbackBind(options.host)) {
12233
12489
  await platform2.write(2, "mcpcat serve: --no-auth is only allowed on loopback\n");
12234
- return 2;
12490
+ return { exitCode: 2 };
12235
12491
  }
12236
12492
  if (options.transport === "http") {
12237
12493
  await platform2.write(
@@ -12239,21 +12495,30 @@ async function runServe(platform2, options, version) {
12239
12495
  "mcpcat serve: warning: running without authentication on loopback\n"
12240
12496
  );
12241
12497
  }
12242
- resolvedBearer = void 0;
12243
- } else if (options.transport === "http") {
12498
+ return { resolved: void 0, generated: false };
12499
+ }
12500
+ if (options.transport === "http") {
12244
12501
  if (platform2.kind === "kernel") {
12245
12502
  await platform2.write(
12246
12503
  2,
12247
12504
  "mcpcat serve: kernel HTTP serve requires --bearer '${VAR}' or --no-auth\n"
12248
12505
  );
12249
- return 2;
12506
+ return { exitCode: 2 };
12250
12507
  }
12251
- resolvedBearer = generateToken();
12252
- generatedBearer = true;
12508
+ return { resolved: generateToken(), generated: true };
12253
12509
  }
12510
+ return { resolved: void 0, generated: false };
12511
+ }
12512
+ async function runServe(platform2, options, version) {
12513
+ const bearerResult = await resolveBearer(platform2, options);
12514
+ if ("exitCode" in bearerResult) return bearerResult.exitCode;
12515
+ const { resolved: resolvedBearer, generated: generatedBearer } = bearerResult;
12254
12516
  if (options.proxy) {
12255
12517
  return runProxyServe(platform2, options, version, resolvedBearer, generatedBearer);
12256
12518
  }
12519
+ if (options.webmcp) {
12520
+ return runWebMcpServe(platform2, options, version, resolvedBearer, generatedBearer);
12521
+ }
12257
12522
  const serveCwd = options.cwd ?? platform2.cwd;
12258
12523
  const childEnv = buildChildEnv(platform2, options.envAllow);
12259
12524
  const discovery = await discoverWithSignal(platform2, options.commands, childEnv, serveCwd);
@@ -12387,6 +12652,233 @@ async function buildProxyConnectOpts(platform2, options, version) {
12387
12652
  }
12388
12653
  return { connectOpts, hiddenValues };
12389
12654
  }
12655
+ function resolvePlaywrightCli(platform2, explicit) {
12656
+ if (explicit) return explicit;
12657
+ if (platform2.kind === "kernel") return "playwright-cli";
12658
+ const pathEnv = platform2.env.PATH ?? "";
12659
+ const sep = pathEnv.includes(";") ? ";" : ":";
12660
+ for (const dir of pathEnv.split(sep)) {
12661
+ if (!dir) continue;
12662
+ return "playwright-cli";
12663
+ }
12664
+ return void 0;
12665
+ }
12666
+ async function runWebMcpServe(platform2, options, version, resolvedBearer, generatedBearer) {
12667
+ const cliPath = resolvePlaywrightCli(platform2, options.webmcpPlaywrightCli);
12668
+ if (!cliPath) {
12669
+ await platform2.write(
12670
+ 2,
12671
+ "mcpcat serve --webmcp: playwright-cli not found. Install it with: curl -fsSL https://get.sliccy.ai/playwright-cli | sh\n"
12672
+ );
12673
+ return 3;
12674
+ }
12675
+ const cdp = options.webmcpCdp ?? platform2.env.SLICC_CDP_URL;
12676
+ const handler = createWebMcpHandler(platform2, {
12677
+ playwrightCli: cliPath,
12678
+ tab: options.webmcpTab,
12679
+ cdp,
12680
+ allowConsequential: options.webmcpAllowConsequential,
12681
+ ttlMs: options.webmcpTtl,
12682
+ callTimeoutMs: options.callTimeout,
12683
+ maxOutput: options.maxOutput
12684
+ });
12685
+ const probe = await handler.getRpcTools();
12686
+ if (probe === null) {
12687
+ await platform2.write(
12688
+ 2,
12689
+ "mcpcat serve --webmcp: playwright-cli not found. Install it with: curl -fsSL https://get.sliccy.ai/playwright-cli | sh\n"
12690
+ );
12691
+ return 3;
12692
+ }
12693
+ const serverName = "mcpcat-webmcp";
12694
+ const getTools = async () => await handler.getRpcTools() ?? [];
12695
+ const callTool = (name, args) => handler.callTool(name, args);
12696
+ if (options.transport === "stdio") {
12697
+ return runWebMcpStdio(platform2, getTools, callTool, serverName, version);
12698
+ }
12699
+ return runWebMcpHttp(
12700
+ platform2,
12701
+ options,
12702
+ getTools,
12703
+ callTool,
12704
+ serverName,
12705
+ version,
12706
+ resolvedBearer,
12707
+ generatedBearer
12708
+ );
12709
+ }
12710
+ async function runWebMcpStdio(platform2, getTools, callTool, serverName, version) {
12711
+ const reader = platform2.stdin.getReader();
12712
+ let buffer = "";
12713
+ const dec = new TextDecoder();
12714
+ const enc = new TextEncoder();
12715
+ try {
12716
+ for (; ; ) {
12717
+ const { done, value } = await reader.read();
12718
+ if (done) break;
12719
+ buffer += dec.decode(value, { stream: true });
12720
+ let nlIdx;
12721
+ while ((nlIdx = buffer.indexOf("\n")) >= 0) {
12722
+ const line = buffer.slice(0, nlIdx).trim();
12723
+ buffer = buffer.slice(nlIdx + 1);
12724
+ if (!line) continue;
12725
+ const tools = await getTools();
12726
+ const outcome = await handleJsonRpc(line, tools, callTool, serverName, version);
12727
+ if (outcome.status === 202) continue;
12728
+ const writer = platform2.stdout.getWriter();
12729
+ await writer.write(enc.encode(`${outcome.body}
12730
+ `));
12731
+ writer.releaseLock();
12732
+ }
12733
+ }
12734
+ } finally {
12735
+ reader.releaseLock();
12736
+ }
12737
+ return 0;
12738
+ }
12739
+ async function runWebMcpHttp(platform2, options, getTools, callTool, serverName, version, resolvedBearer, generatedBearer) {
12740
+ if (!platform2.listen) {
12741
+ await platform2.write(2, "mcpcat serve --webmcp: HTTP transport requires platform.listen\n");
12742
+ return 3;
12743
+ }
12744
+ const boundLoopback = isLoopbackBind(options.host);
12745
+ if (!boundLoopback && !resolvedBearer) {
12746
+ await platform2.write(
12747
+ 2,
12748
+ "mcpcat serve --webmcp: --bearer is required when binding off loopback\n"
12749
+ );
12750
+ return 2;
12751
+ }
12752
+ const allowedOrigins = new Set(options.allowOrigins);
12753
+ const listener = await platform2.listen({ host: options.host, port: options.port });
12754
+ const actualPort = listener.port;
12755
+ const serverOrigins = buildServerOrigins(options.host, actualPort);
12756
+ const allowedHosts = buildAllowedHosts(options.host, actualPort, options.allowHosts);
12757
+ const listenUrl = `http://${options.host}:${actualPort}/mcp`;
12758
+ let tokenFilePath;
12759
+ if (generatedBearer && resolvedBearer) {
12760
+ tokenFilePath = await publishBearer(
12761
+ platform2,
12762
+ resolvedBearer,
12763
+ actualPort,
12764
+ listenUrl,
12765
+ options.json
12766
+ );
12767
+ } else {
12768
+ if (options.json) {
12769
+ await platform2.write(1, `${JSON.stringify({ url: listenUrl })}
12770
+ `);
12771
+ }
12772
+ }
12773
+ await platform2.write(2, `mcpcat serve --webmcp: listening on ${listenUrl}
12774
+ `);
12775
+ if (options.onListening) {
12776
+ await options.onListening(actualPort);
12777
+ }
12778
+ let shutdownRequested = false;
12779
+ const shutdown = () => {
12780
+ if (shutdownRequested) return;
12781
+ shutdownRequested = true;
12782
+ listener.close();
12783
+ if (tokenFilePath) {
12784
+ platform2.fs.unlink?.(tokenFilePath).catch(() => {
12785
+ });
12786
+ }
12787
+ };
12788
+ if (platform2.onSignal) {
12789
+ platform2.onSignal("SIGINT", shutdown);
12790
+ platform2.onSignal("SIGTERM", shutdown);
12791
+ platform2.onSignal("SIGHUP", shutdown);
12792
+ }
12793
+ const handler = async (request) => {
12794
+ if (request.target !== "/mcp") {
12795
+ return {
12796
+ status: 404,
12797
+ reason: "Not Found",
12798
+ headers: { "content-type": "text/plain" },
12799
+ body: "Not Found"
12800
+ };
12801
+ }
12802
+ if (request.method !== "POST") {
12803
+ return {
12804
+ status: 405,
12805
+ reason: "Method Not Allowed",
12806
+ headers: { "content-type": "text/plain", allow: "POST" },
12807
+ body: "Method Not Allowed"
12808
+ };
12809
+ }
12810
+ if (!checkContentType(request.headers["content-type"])) {
12811
+ return {
12812
+ status: 415,
12813
+ reason: "Unsupported Media Type",
12814
+ headers: { "content-type": "text/plain" },
12815
+ body: "Content-Type must be application/json"
12816
+ };
12817
+ }
12818
+ if (!checkAcceptHeader(request.headers.accept)) {
12819
+ return {
12820
+ status: 406,
12821
+ reason: "Not Acceptable",
12822
+ headers: { "content-type": "text/plain" },
12823
+ body: "Accept must include application/json or text/event-stream"
12824
+ };
12825
+ }
12826
+ const originCheck = checkOrigin(request.headers.origin, serverOrigins, allowedOrigins);
12827
+ if (!originCheck.ok) {
12828
+ return {
12829
+ status: 403,
12830
+ reason: "Forbidden",
12831
+ headers: { "content-type": "text/plain" },
12832
+ body: originCheck.reason ?? "Forbidden"
12833
+ };
12834
+ }
12835
+ const hostCheck = checkHost(request.headers.host, allowedHosts);
12836
+ if (!hostCheck.ok) {
12837
+ return {
12838
+ status: 403,
12839
+ reason: "Forbidden",
12840
+ headers: { "content-type": "text/plain" },
12841
+ body: hostCheck.reason ?? "Forbidden"
12842
+ };
12843
+ }
12844
+ if (resolvedBearer) {
12845
+ const authHeader = request.headers.authorization;
12846
+ const provided = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : "";
12847
+ if (!provided || !constantTimeEqual(provided, resolvedBearer)) {
12848
+ return {
12849
+ status: 401,
12850
+ reason: "Unauthorized",
12851
+ headers: { "content-type": "text/plain" },
12852
+ body: "Unauthorized"
12853
+ };
12854
+ }
12855
+ }
12856
+ const tools = await getTools();
12857
+ const outcome = await handleJsonRpc(request.body, tools, callTool, serverName, version);
12858
+ return {
12859
+ status: outcome.status,
12860
+ reason: outcome.status === 200 ? "OK" : "Accepted",
12861
+ headers: { "content-type": outcome.contentType },
12862
+ body: outcome.body
12863
+ };
12864
+ };
12865
+ try {
12866
+ for await (const connection of listener) {
12867
+ if (shutdownRequested) break;
12868
+ const conn = connection;
12869
+ handleConnection(conn.readable, conn.writable, handler).catch(() => {
12870
+ }).finally(() => {
12871
+ try {
12872
+ conn.close();
12873
+ } catch {
12874
+ }
12875
+ });
12876
+ }
12877
+ } catch {
12878
+ if (!shutdownRequested) throw new Error("listener closed unexpectedly");
12879
+ }
12880
+ return 0;
12881
+ }
12390
12882
  async function runProxyServe(platform2, options, version, resolvedBearer, generatedBearer) {
12391
12883
  const built = await buildProxyConnectOpts(platform2, options, version);
12392
12884
  if ("error" in built) {
@@ -13598,7 +14090,10 @@ async function sealSecret(platform2, name, value, options) {
13598
14090
  }
13599
14091
  const response = JSON.parse(responseText);
13600
14092
  if (response.error) {
13601
- throw Object.assign(new Error(response.error), { sealErrno: response.errno ?? "EACCES" });
14093
+ const isStructured = typeof response.error === "object" && response.error !== null && response.error.code;
14094
+ const errno = isStructured ? response.error.code : response.errno ?? "EACCES";
14095
+ const message = isStructured ? response.error.message : response.error;
14096
+ throw Object.assign(new Error(String(message)), { sealErrno: errno });
13602
14097
  }
13603
14098
  return { placeholder: response.placeholder };
13604
14099
  }
@@ -13610,6 +14105,7 @@ function sealErrorMessage(errno, name) {
13610
14105
  if (errno === "EEXIST") return `a non-bridge secret named ${name} already exists`;
13611
14106
  if (errno === "ENOTCONN") return "no vault available; use --bearer '${VAR}' or --no-auth";
13612
14107
  if (errno === "EACCES") return "vault locked; use --bearer '${VAR}' or --no-auth";
14108
+ if (errno === "ETIMEDOUT") return "vault did not respond in time";
13613
14109
  return `vault error: ${errno}`;
13614
14110
  }
13615
14111
 
@@ -14965,7 +15461,7 @@ async function dispatchDiscoveryVerb(platform2, parsed, args) {
14965
15461
  async function dispatchServeVerb(platform2, args, version) {
14966
15462
  const serveArgs = args.slice(args.indexOf("serve") + 1);
14967
15463
  const serveOptions = parseServeFlags(serveArgs);
14968
- if (!serveOptions.proxy && serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
15464
+ if (!serveOptions.proxy && !serveOptions.webmcp && serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
14969
15465
  await platform2.write(2, "mcpcat serve: no commands or --tool templates given\n");
14970
15466
  return 2;
14971
15467
  }
@@ -15237,6 +15733,9 @@ function classifyError(err) {
15237
15733
  if (err instanceof InputRequiredError) {
15238
15734
  return { code: EXIT_INPUT_REQUIRED, kind: "input_required", msg: err.requestJson };
15239
15735
  }
15736
+ if (err instanceof TransportRefusalError) {
15737
+ return { code: EXIT_CONNECTION, kind: "connection", msg: err.reason };
15738
+ }
15240
15739
  if (err instanceof McpTimeoutError2) {
15241
15740
  return { code: EXIT_TIMEOUT, kind: "timeout", msg: err.message };
15242
15741
  }
@@ -15293,6 +15792,14 @@ async function resolveAuthProvider(platform2, hasAuthHeader, url, target, config
15293
15792
  return { provider, credentialSecrets };
15294
15793
  }
15295
15794
  function classifyConnectionError(err, target, interrupted) {
15795
+ if (err instanceof TransportRefusalError) {
15796
+ const hint = "in seven, reach the host via http://host.slicc.internal:<port>; in tests, enable the transport's hostLoopback";
15797
+ return {
15798
+ code: EXIT_CONNECTION,
15799
+ kind: "connection",
15800
+ msg: `${err.reason} (${hint})`
15801
+ };
15802
+ }
15296
15803
  if (err instanceof McpAuthError || err instanceof AuthStepUpError || err instanceof JerkSignInRequiredError) {
15297
15804
  return classifyError(err);
15298
15805
  }
@@ -15841,7 +16348,7 @@ async function main(ctx) {
15841
16348
  packageDir: "/node_modules/@ai-ecoverse/mcpcat"
15842
16349
  });
15843
16350
  const args = ctx.argv.slice(1);
15844
- return run(platform2, "0.14.7", args);
16351
+ return run(platform2, "0.15.1", args);
15845
16352
  }
15846
16353
  export {
15847
16354
  main as default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/mcpcat",
3
- "version": "0.14.7",
3
+ "version": "0.15.1",
4
4
  "description": "A curl for the Model Context Protocol",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -56,7 +56,7 @@
56
56
  "trust:dry": "npx fledgling sync --dry-run -y --skip-publish"
57
57
  },
58
58
  "devDependencies": {
59
- "@ai-ecoverse/slicc-kernel": "1.83.0",
59
+ "@ai-ecoverse/slicc-kernel": "1.85.0",
60
60
  "@ai-ecoverse/slicc-shared-web": "1.13.1",
61
61
  "@biomejs/biome": "2.5.15",
62
62
  "@earendil-works/pi-codemode": "1.2.0",