@ai-ecoverse/mcpcat 0.14.6 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6793,7 +6793,13 @@ var CLIENT_NAME = "mcpcat";
6793
6793
  var CALLBACK_HOST = "127.0.0.1";
6794
6794
  var CALLBACK_PATH = "/callback";
6795
6795
  var REQUEST_TIMEOUT_MS = 15e3;
6796
- var REFRESH_SKEW_MS = 3e4;
6796
+ var MAX_REFRESH_SKEW_MS = 3e4;
6797
+ function computeSkew(state) {
6798
+ const expiresIn = state?.tokens?.expires_in;
6799
+ if (typeof expiresIn !== "number" || expiresIn <= 0) return MAX_REFRESH_SKEW_MS;
6800
+ const ttlMs = expiresIn * 1e3;
6801
+ return Math.min(MAX_REFRESH_SKEW_MS, Math.floor(ttlMs / 2));
6802
+ }
6797
6803
  var FALLBACK_REDIRECT_URL = `http://${CALLBACK_HOST}${CALLBACK_PATH}`;
6798
6804
  function timedFetch(fetchImpl) {
6799
6805
  return (input, init) => {
@@ -6941,7 +6947,8 @@ function createPiAuthProvider(options) {
6941
6947
  cachedToken = state?.tokens?.access_token;
6942
6948
  registerCredential(cachedToken);
6943
6949
  if (state?.tokens?.refresh_token) registerCredential(state.tokens.refresh_token);
6944
- const expired = state?.tokensExpireAt !== void 0 && state.tokensExpireAt - REFRESH_SKEW_MS <= Date.now();
6950
+ const skew = computeSkew(state);
6951
+ const expired = state?.tokensExpireAt !== void 0 && state.tokensExpireAt - skew <= Date.now();
6945
6952
  if (expired && state?.tokens?.refresh_token) {
6946
6953
  await refresh(cachedToken).catch(() => void 0);
6947
6954
  const refreshedState = await store.load();
@@ -11796,7 +11803,8 @@ ${stderr}` : stderr : stdout;
11796
11803
  return {
11797
11804
  content: [{ type: "text", text }],
11798
11805
  isError: result.exitCode !== 0,
11799
- structuredContent: { stdout, stderr, exitCode: result.exitCode }
11806
+ structuredContent: { stdout, stderr, exitCode: result.exitCode },
11807
+ ...result.meta ? { _meta: result.meta } : {}
11800
11808
  };
11801
11809
  }
11802
11810
  function initializeResult(params, serverName, serverVersion) {
@@ -12096,6 +12104,219 @@ function createProxyHandler(options) {
12096
12104
  return { handle, shutdown };
12097
12105
  }
12098
12106
 
12107
+ // src/serve/webmcp.ts
12108
+ function buildBaseArgs(options) {
12109
+ const args2 = [];
12110
+ if (options.cdp) {
12111
+ args2.push("--cdp", options.cdp);
12112
+ }
12113
+ if (options.tab) {
12114
+ args2.push("--tab", options.tab);
12115
+ }
12116
+ return args2;
12117
+ }
12118
+ function generateNonce() {
12119
+ const bytes = new Uint8Array(8);
12120
+ crypto.getRandomValues(bytes);
12121
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
12122
+ }
12123
+ function wrapUntrusted(content) {
12124
+ const nonce = generateNonce();
12125
+ const openTag = `<<untrusted-page-content id=${nonce}>>`;
12126
+ const closeTag = `<<end id=${nonce}>>`;
12127
+ const neutralised = content.replaceAll("<<untrusted-page-content", "< <untrusted-page-content");
12128
+ return `${openTag}
12129
+ ${neutralised}
12130
+ ${closeTag}`;
12131
+ }
12132
+ async function runProcess(platform2, argv, stdin, timeoutMs) {
12133
+ let proc;
12134
+ try {
12135
+ proc = await platform2.spawn({ argv });
12136
+ } catch (err) {
12137
+ const code2 = err.code;
12138
+ if (code2 === "ENOENT" || code2 === "EACCES") {
12139
+ return { stdout: "", stderr: `command not found: ${argv[0]}`, exitCode: -1 };
12140
+ }
12141
+ throw err;
12142
+ }
12143
+ if (stdin !== void 0) {
12144
+ const writer = proc.stdin.getWriter();
12145
+ await writer.write(new TextEncoder().encode(stdin));
12146
+ await writer.close();
12147
+ } else {
12148
+ try {
12149
+ await proc.stdin.getWriter().close();
12150
+ } catch {
12151
+ }
12152
+ }
12153
+ const stdoutChunks = [];
12154
+ const stderrChunks = [];
12155
+ const readStream3 = async (stream, into) => {
12156
+ const r = stream.getReader();
12157
+ try {
12158
+ for (; ; ) {
12159
+ const { done, value } = await r.read();
12160
+ if (done) break;
12161
+ into.push(value);
12162
+ }
12163
+ } finally {
12164
+ r.releaseLock();
12165
+ }
12166
+ };
12167
+ const timer = new Promise(
12168
+ (resolve) => setTimeout(() => resolve("timeout"), timeoutMs ?? 3e4)
12169
+ );
12170
+ const work = Promise.all([
12171
+ readStream3(proc.stdout, stdoutChunks),
12172
+ readStream3(proc.stderr, stderrChunks),
12173
+ proc.wait()
12174
+ ]);
12175
+ const result = await Promise.race([work, timer]);
12176
+ if (result === "timeout") {
12177
+ await proc.kill("SIGTERM");
12178
+ return { stdout: "", stderr: "timed out", exitCode: 124 };
12179
+ }
12180
+ const [, , exitResult] = result;
12181
+ const dec = new TextDecoder();
12182
+ const concat2 = (chunks) => {
12183
+ const total = chunks.reduce((s, c) => s + c.length, 0);
12184
+ const out = new Uint8Array(total);
12185
+ let offset = 0;
12186
+ for (const c of chunks) {
12187
+ out.set(c, offset);
12188
+ offset += c.length;
12189
+ }
12190
+ return out;
12191
+ };
12192
+ return {
12193
+ stdout: dec.decode(concat2(stdoutChunks)),
12194
+ stderr: dec.decode(concat2(stderrChunks)),
12195
+ exitCode: exitResult.status
12196
+ };
12197
+ }
12198
+ function createWebMcpHandler(platform2, options) {
12199
+ let cache;
12200
+ const baseArgs = buildBaseArgs(options);
12201
+ async function listRaw() {
12202
+ const now = Date.now();
12203
+ if (cache && now < cache.expiresAt) {
12204
+ return { tools: cache.tools };
12205
+ }
12206
+ const argv = [options.playwrightCli, "webmcp-list", "--json", ...baseArgs];
12207
+ const result = await runProcess(platform2, argv, void 0, options.callTimeoutMs);
12208
+ if (result.exitCode === -1) {
12209
+ return { tools: [], error: result.stderr, notFound: true };
12210
+ }
12211
+ if (result.exitCode !== 0) {
12212
+ return { tools: [], error: result.stderr || `webmcp-list exited ${result.exitCode}` };
12213
+ }
12214
+ let parsed;
12215
+ try {
12216
+ parsed = JSON.parse(result.stdout);
12217
+ } catch {
12218
+ return { tools: [], error: "webmcp-list returned invalid JSON" };
12219
+ }
12220
+ if (!Array.isArray(parsed)) {
12221
+ return { tools: [], error: "webmcp-list did not return an array" };
12222
+ }
12223
+ const tools = parsed;
12224
+ cache = { tools, expiresAt: now + options.ttlMs };
12225
+ return { tools };
12226
+ }
12227
+ function mapDescriptionAnnotations(ann) {
12228
+ const mapped = { openWorldHint: true };
12229
+ if (!ann) return mapped;
12230
+ if (ann.readOnlyHint !== void 0) mapped.readOnlyHint = ann.readOnlyHint;
12231
+ if (ann.consequentialHint) mapped.destructiveHint = true;
12232
+ return mapped;
12233
+ }
12234
+ function originPrefix(tools) {
12235
+ const origins = new Set(tools.map((t) => t.origin));
12236
+ return origins.size > 1;
12237
+ }
12238
+ function hostFromOrigin(origin) {
12239
+ try {
12240
+ return new URL(origin).host;
12241
+ } catch {
12242
+ return origin;
12243
+ }
12244
+ }
12245
+ function buildDescription(tool) {
12246
+ const parts = [];
12247
+ if (tool.description) parts.push(tool.description);
12248
+ parts.push(`[origin: ${tool.origin}]`);
12249
+ return parts.join("\n");
12250
+ }
12251
+ function buildToolName(tool, prefix) {
12252
+ const base = tool.name;
12253
+ if (!prefix) return base;
12254
+ const host = hostFromOrigin(tool.origin).replace(/[^a-zA-Z0-9_]/g, "_");
12255
+ return `${host}__${base}`;
12256
+ }
12257
+ async function getRpcTools() {
12258
+ const { tools, error, notFound } = await listRaw();
12259
+ if (notFound) return null;
12260
+ if (error) return [];
12261
+ const needPrefix = originPrefix(tools);
12262
+ const rpcTools = [];
12263
+ for (const tool of tools) {
12264
+ if (tool.annotations?.consequentialHint && !options.allowConsequential) {
12265
+ continue;
12266
+ }
12267
+ const mapped = mapDescriptionAnnotations(tool.annotations);
12268
+ rpcTools.push({
12269
+ name: buildToolName(tool, needPrefix),
12270
+ description: buildDescription(tool),
12271
+ inputSchema: tool.inputSchema,
12272
+ annotations: mapped
12273
+ });
12274
+ }
12275
+ return rpcTools;
12276
+ }
12277
+ async function callTool(name, args2) {
12278
+ const { tools, error } = await listRaw();
12279
+ if (error) {
12280
+ return { stdout: "", stderr: error, exitCode: 2 };
12281
+ }
12282
+ const needPrefix = originPrefix(tools);
12283
+ const tool = tools.find((t) => buildToolName(t, needPrefix) === name);
12284
+ if (!tool) {
12285
+ return { stdout: "", stderr: `unknown tool: ${name}`, exitCode: 2 };
12286
+ }
12287
+ const argv = [options.playwrightCli, "webmcp-call", tool.name, "-", ...baseArgs];
12288
+ if (options.allowConsequential) {
12289
+ argv.push("--allow-consequential");
12290
+ }
12291
+ const stdinData = JSON.stringify(args2 ?? {});
12292
+ const result = await runProcess(platform2, argv, stdinData, options.callTimeoutMs);
12293
+ if (result.exitCode === -1) {
12294
+ throw new Error(result.stderr || "playwright-cli not found");
12295
+ }
12296
+ if (result.exitCode === 2) {
12297
+ throw new Error(result.stderr || "webmcp-call error");
12298
+ }
12299
+ const meta = {
12300
+ "io.mcpcat/untrusted": true,
12301
+ origin: tool.origin
12302
+ };
12303
+ if (result.exitCode === 1) {
12304
+ return {
12305
+ stdout: "",
12306
+ stderr: wrapUntrusted(result.stderr || result.stdout),
12307
+ exitCode: 1,
12308
+ meta
12309
+ };
12310
+ }
12311
+ const text = wrapUntrusted(result.stdout);
12312
+ return { stdout: text, stderr: result.stderr, exitCode: 0, meta };
12313
+ }
12314
+ function invalidateCache() {
12315
+ cache = void 0;
12316
+ }
12317
+ return { getRpcTools, callTool, invalidateCache };
12318
+ }
12319
+
12099
12320
  // src/serve/runtime.ts
12100
12321
  var CALL_TIMEOUT_MS = 12e4;
12101
12322
  var MAX_OUTPUT_DEFAULT = 1048576;
@@ -12130,6 +12351,10 @@ function parseSingleFlag(args2, i, opts) {
12130
12351
  opts.json = true;
12131
12352
  } else if (arg === "--no-auth") {
12132
12353
  opts.noAuth = true;
12354
+ } else if (arg === "--webmcp") {
12355
+ opts.webmcp = true;
12356
+ } else if (arg === "--allow-consequential") {
12357
+ opts.webmcpAllowConsequential = true;
12133
12358
  } else if (!arg.startsWith("-")) {
12134
12359
  opts.commands.push(parseCommandTarget(arg));
12135
12360
  } else {
@@ -12171,6 +12396,18 @@ function parseValueFlag(args2, i, opts) {
12171
12396
  case "--cwd":
12172
12397
  opts.cwd = args2[++i];
12173
12398
  break;
12399
+ case "--tab":
12400
+ opts.webmcpTab = args2[++i];
12401
+ break;
12402
+ case "--webmcp-ttl":
12403
+ opts.webmcpTtl = Number(args2[++i]) * 1e3;
12404
+ break;
12405
+ case "--playwright-cli":
12406
+ opts.webmcpPlaywrightCli = args2[++i];
12407
+ break;
12408
+ case "--cdp":
12409
+ opts.webmcpCdp = args2[++i];
12410
+ break;
12174
12411
  case "-H":
12175
12412
  case "--header": {
12176
12413
  const hdr = args2[++i];
@@ -12203,7 +12440,13 @@ function parseServeFlags(args2) {
12203
12440
  json: false,
12204
12441
  commands: [],
12205
12442
  proxy: void 0,
12206
- proxyHeaders: []
12443
+ proxyHeaders: [],
12444
+ webmcp: false,
12445
+ webmcpTab: void 0,
12446
+ webmcpTtl: 5e3,
12447
+ webmcpAllowConsequential: false,
12448
+ webmcpPlaywrightCli: void 0,
12449
+ webmcpCdp: void 0
12207
12450
  };
12208
12451
  for (let i = 0; i < args2.length; i++) {
12209
12452
  i = parseSingleFlag(args2, i, opts);
@@ -12585,25 +12828,25 @@ async function discoverWithSignal(platform2, commands, childEnv, cwd) {
12585
12828
  if (earlyShutdown) return { exitCode: 143, inflightDone };
12586
12829
  return { clis };
12587
12830
  }
12588
- async function runServe(platform2, options, version) {
12589
- let resolvedBearer;
12590
- let generatedBearer = false;
12831
+ async function resolveBearer(platform2, options) {
12591
12832
  if (options._resolvedBearer) {
12592
- resolvedBearer = options._resolvedBearer;
12593
- } else if (options.bearer) {
12833
+ return { resolved: options._resolvedBearer, generated: false };
12834
+ }
12835
+ if (options.bearer) {
12594
12836
  const expanded = expandBearerToken(options.bearer, platform2.env);
12595
12837
  if (!expanded) {
12596
12838
  await platform2.write(
12597
12839
  2,
12598
12840
  "mcpcat serve: --bearer requires a ${NAME} variable reference, not a literal token. Use --bearer '${MY_TOKEN}' and set MY_TOKEN in the environment.\n"
12599
12841
  );
12600
- return 2;
12842
+ return { exitCode: 2 };
12601
12843
  }
12602
- resolvedBearer = expanded;
12603
- } else if (options.noAuth) {
12844
+ return { resolved: expanded, generated: false };
12845
+ }
12846
+ if (options.noAuth) {
12604
12847
  if (options.transport === "http" && !isLoopbackBind(options.host)) {
12605
12848
  await platform2.write(2, "mcpcat serve: --no-auth is only allowed on loopback\n");
12606
- return 2;
12849
+ return { exitCode: 2 };
12607
12850
  }
12608
12851
  if (options.transport === "http") {
12609
12852
  await platform2.write(
@@ -12611,21 +12854,30 @@ async function runServe(platform2, options, version) {
12611
12854
  "mcpcat serve: warning: running without authentication on loopback\n"
12612
12855
  );
12613
12856
  }
12614
- resolvedBearer = void 0;
12615
- } else if (options.transport === "http") {
12857
+ return { resolved: void 0, generated: false };
12858
+ }
12859
+ if (options.transport === "http") {
12616
12860
  if (platform2.kind === "kernel") {
12617
12861
  await platform2.write(
12618
12862
  2,
12619
12863
  "mcpcat serve: kernel HTTP serve requires --bearer '${VAR}' or --no-auth\n"
12620
12864
  );
12621
- return 2;
12865
+ return { exitCode: 2 };
12622
12866
  }
12623
- resolvedBearer = generateToken();
12624
- generatedBearer = true;
12867
+ return { resolved: generateToken(), generated: true };
12625
12868
  }
12869
+ return { resolved: void 0, generated: false };
12870
+ }
12871
+ async function runServe(platform2, options, version) {
12872
+ const bearerResult = await resolveBearer(platform2, options);
12873
+ if ("exitCode" in bearerResult) return bearerResult.exitCode;
12874
+ const { resolved: resolvedBearer, generated: generatedBearer } = bearerResult;
12626
12875
  if (options.proxy) {
12627
12876
  return runProxyServe(platform2, options, version, resolvedBearer, generatedBearer);
12628
12877
  }
12878
+ if (options.webmcp) {
12879
+ return runWebMcpServe(platform2, options, version, resolvedBearer, generatedBearer);
12880
+ }
12629
12881
  const serveCwd = options.cwd ?? platform2.cwd;
12630
12882
  const childEnv = buildChildEnv(platform2, options.envAllow);
12631
12883
  const discovery = await discoverWithSignal(platform2, options.commands, childEnv, serveCwd);
@@ -12759,6 +13011,233 @@ async function buildProxyConnectOpts(platform2, options, version) {
12759
13011
  }
12760
13012
  return { connectOpts, hiddenValues };
12761
13013
  }
13014
+ function resolvePlaywrightCli(platform2, explicit) {
13015
+ if (explicit) return explicit;
13016
+ if (platform2.kind === "kernel") return "playwright-cli";
13017
+ const pathEnv = platform2.env.PATH ?? "";
13018
+ const sep = pathEnv.includes(";") ? ";" : ":";
13019
+ for (const dir of pathEnv.split(sep)) {
13020
+ if (!dir) continue;
13021
+ return "playwright-cli";
13022
+ }
13023
+ return void 0;
13024
+ }
13025
+ async function runWebMcpServe(platform2, options, version, resolvedBearer, generatedBearer) {
13026
+ const cliPath = resolvePlaywrightCli(platform2, options.webmcpPlaywrightCli);
13027
+ if (!cliPath) {
13028
+ await platform2.write(
13029
+ 2,
13030
+ "mcpcat serve --webmcp: playwright-cli not found. Install it with: curl -fsSL https://get.sliccy.ai/playwright-cli | sh\n"
13031
+ );
13032
+ return 3;
13033
+ }
13034
+ const cdp = options.webmcpCdp ?? platform2.env.SLICC_CDP_URL;
13035
+ const handler = createWebMcpHandler(platform2, {
13036
+ playwrightCli: cliPath,
13037
+ tab: options.webmcpTab,
13038
+ cdp,
13039
+ allowConsequential: options.webmcpAllowConsequential,
13040
+ ttlMs: options.webmcpTtl,
13041
+ callTimeoutMs: options.callTimeout,
13042
+ maxOutput: options.maxOutput
13043
+ });
13044
+ const probe = await handler.getRpcTools();
13045
+ if (probe === null) {
13046
+ await platform2.write(
13047
+ 2,
13048
+ "mcpcat serve --webmcp: playwright-cli not found. Install it with: curl -fsSL https://get.sliccy.ai/playwright-cli | sh\n"
13049
+ );
13050
+ return 3;
13051
+ }
13052
+ const serverName = "mcpcat-webmcp";
13053
+ const getTools = async () => await handler.getRpcTools() ?? [];
13054
+ const callTool = (name, args2) => handler.callTool(name, args2);
13055
+ if (options.transport === "stdio") {
13056
+ return runWebMcpStdio(platform2, getTools, callTool, serverName, version);
13057
+ }
13058
+ return runWebMcpHttp(
13059
+ platform2,
13060
+ options,
13061
+ getTools,
13062
+ callTool,
13063
+ serverName,
13064
+ version,
13065
+ resolvedBearer,
13066
+ generatedBearer
13067
+ );
13068
+ }
13069
+ async function runWebMcpStdio(platform2, getTools, callTool, serverName, version) {
13070
+ const reader = platform2.stdin.getReader();
13071
+ let buffer = "";
13072
+ const dec = new TextDecoder();
13073
+ const enc = new TextEncoder();
13074
+ try {
13075
+ for (; ; ) {
13076
+ const { done, value } = await reader.read();
13077
+ if (done) break;
13078
+ buffer += dec.decode(value, { stream: true });
13079
+ let nlIdx;
13080
+ while ((nlIdx = buffer.indexOf("\n")) >= 0) {
13081
+ const line = buffer.slice(0, nlIdx).trim();
13082
+ buffer = buffer.slice(nlIdx + 1);
13083
+ if (!line) continue;
13084
+ const tools = await getTools();
13085
+ const outcome = await handleJsonRpc(line, tools, callTool, serverName, version);
13086
+ if (outcome.status === 202) continue;
13087
+ const writer = platform2.stdout.getWriter();
13088
+ await writer.write(enc.encode(`${outcome.body}
13089
+ `));
13090
+ writer.releaseLock();
13091
+ }
13092
+ }
13093
+ } finally {
13094
+ reader.releaseLock();
13095
+ }
13096
+ return 0;
13097
+ }
13098
+ async function runWebMcpHttp(platform2, options, getTools, callTool, serverName, version, resolvedBearer, generatedBearer) {
13099
+ if (!platform2.listen) {
13100
+ await platform2.write(2, "mcpcat serve --webmcp: HTTP transport requires platform.listen\n");
13101
+ return 3;
13102
+ }
13103
+ const boundLoopback = isLoopbackBind(options.host);
13104
+ if (!boundLoopback && !resolvedBearer) {
13105
+ await platform2.write(
13106
+ 2,
13107
+ "mcpcat serve --webmcp: --bearer is required when binding off loopback\n"
13108
+ );
13109
+ return 2;
13110
+ }
13111
+ const allowedOrigins = new Set(options.allowOrigins);
13112
+ const listener = await platform2.listen({ host: options.host, port: options.port });
13113
+ const actualPort = listener.port;
13114
+ const serverOrigins = buildServerOrigins(options.host, actualPort);
13115
+ const allowedHosts = buildAllowedHosts(options.host, actualPort, options.allowHosts);
13116
+ const listenUrl = `http://${options.host}:${actualPort}/mcp`;
13117
+ let tokenFilePath;
13118
+ if (generatedBearer && resolvedBearer) {
13119
+ tokenFilePath = await publishBearer(
13120
+ platform2,
13121
+ resolvedBearer,
13122
+ actualPort,
13123
+ listenUrl,
13124
+ options.json
13125
+ );
13126
+ } else {
13127
+ if (options.json) {
13128
+ await platform2.write(1, `${JSON.stringify({ url: listenUrl })}
13129
+ `);
13130
+ }
13131
+ }
13132
+ await platform2.write(2, `mcpcat serve --webmcp: listening on ${listenUrl}
13133
+ `);
13134
+ if (options.onListening) {
13135
+ await options.onListening(actualPort);
13136
+ }
13137
+ let shutdownRequested = false;
13138
+ const shutdown = () => {
13139
+ if (shutdownRequested) return;
13140
+ shutdownRequested = true;
13141
+ listener.close();
13142
+ if (tokenFilePath) {
13143
+ platform2.fs.unlink?.(tokenFilePath).catch(() => {
13144
+ });
13145
+ }
13146
+ };
13147
+ if (platform2.onSignal) {
13148
+ platform2.onSignal("SIGINT", shutdown);
13149
+ platform2.onSignal("SIGTERM", shutdown);
13150
+ platform2.onSignal("SIGHUP", shutdown);
13151
+ }
13152
+ const handler = async (request) => {
13153
+ if (request.target !== "/mcp") {
13154
+ return {
13155
+ status: 404,
13156
+ reason: "Not Found",
13157
+ headers: { "content-type": "text/plain" },
13158
+ body: "Not Found"
13159
+ };
13160
+ }
13161
+ if (request.method !== "POST") {
13162
+ return {
13163
+ status: 405,
13164
+ reason: "Method Not Allowed",
13165
+ headers: { "content-type": "text/plain", allow: "POST" },
13166
+ body: "Method Not Allowed"
13167
+ };
13168
+ }
13169
+ if (!checkContentType(request.headers["content-type"])) {
13170
+ return {
13171
+ status: 415,
13172
+ reason: "Unsupported Media Type",
13173
+ headers: { "content-type": "text/plain" },
13174
+ body: "Content-Type must be application/json"
13175
+ };
13176
+ }
13177
+ if (!checkAcceptHeader(request.headers.accept)) {
13178
+ return {
13179
+ status: 406,
13180
+ reason: "Not Acceptable",
13181
+ headers: { "content-type": "text/plain" },
13182
+ body: "Accept must include application/json or text/event-stream"
13183
+ };
13184
+ }
13185
+ const originCheck = checkOrigin(request.headers.origin, serverOrigins, allowedOrigins);
13186
+ if (!originCheck.ok) {
13187
+ return {
13188
+ status: 403,
13189
+ reason: "Forbidden",
13190
+ headers: { "content-type": "text/plain" },
13191
+ body: originCheck.reason ?? "Forbidden"
13192
+ };
13193
+ }
13194
+ const hostCheck = checkHost(request.headers.host, allowedHosts);
13195
+ if (!hostCheck.ok) {
13196
+ return {
13197
+ status: 403,
13198
+ reason: "Forbidden",
13199
+ headers: { "content-type": "text/plain" },
13200
+ body: hostCheck.reason ?? "Forbidden"
13201
+ };
13202
+ }
13203
+ if (resolvedBearer) {
13204
+ const authHeader = request.headers.authorization;
13205
+ const provided = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : "";
13206
+ if (!provided || !constantTimeEqual(provided, resolvedBearer)) {
13207
+ return {
13208
+ status: 401,
13209
+ reason: "Unauthorized",
13210
+ headers: { "content-type": "text/plain" },
13211
+ body: "Unauthorized"
13212
+ };
13213
+ }
13214
+ }
13215
+ const tools = await getTools();
13216
+ const outcome = await handleJsonRpc(request.body, tools, callTool, serverName, version);
13217
+ return {
13218
+ status: outcome.status,
13219
+ reason: outcome.status === 200 ? "OK" : "Accepted",
13220
+ headers: { "content-type": outcome.contentType },
13221
+ body: outcome.body
13222
+ };
13223
+ };
13224
+ try {
13225
+ for await (const connection of listener) {
13226
+ if (shutdownRequested) break;
13227
+ const conn = connection;
13228
+ handleConnection(conn.readable, conn.writable, handler).catch(() => {
13229
+ }).finally(() => {
13230
+ try {
13231
+ conn.close();
13232
+ } catch {
13233
+ }
13234
+ });
13235
+ }
13236
+ } catch {
13237
+ if (!shutdownRequested) throw new Error("listener closed unexpectedly");
13238
+ }
13239
+ return 0;
13240
+ }
12762
13241
  async function runProxyServe(platform2, options, version, resolvedBearer, generatedBearer) {
12763
13242
  const built = await buildProxyConnectOpts(platform2, options, version);
12764
13243
  if ("error" in built) {
@@ -15337,7 +15816,7 @@ async function dispatchDiscoveryVerb(platform2, parsed, args2) {
15337
15816
  async function dispatchServeVerb(platform2, args2, version) {
15338
15817
  const serveArgs = args2.slice(args2.indexOf("serve") + 1);
15339
15818
  const serveOptions = parseServeFlags(serveArgs);
15340
- if (!serveOptions.proxy && serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
15819
+ if (!serveOptions.proxy && !serveOptions.webmcp && serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
15341
15820
  await platform2.write(2, "mcpcat serve: no commands or --tool templates given\n");
15342
15821
  return 2;
15343
15822
  }
@@ -15708,7 +16187,7 @@ async function handleInputRequired(platform2, conn, err, respondData, json) {
15708
16187
  `);
15709
16188
  return EXIT_INPUT_REQUIRED;
15710
16189
  }
15711
- const isTty = platform2.isatty(0) && platform2.isatty(2);
16190
+ const isTty = platform2.kind === "host" && platform2.isatty(0) && platform2.isatty(2);
15712
16191
  const elicitResult = await handleElicitation(platform2, inputReq, isTty, respondData);
15713
16192
  if ("exitCode" in elicitResult) {
15714
16193
  await platform2.write(2, `${err.requestJson}
@@ -16170,5 +16649,5 @@ try {
16170
16649
  });
16171
16650
  } catch {
16172
16651
  }
16173
- var code = await run(platform, "0.14.6", args);
16652
+ var code = await run(platform, "0.15.0", args);
16174
16653
  process.exit(code);
@@ -6414,7 +6414,13 @@ var CLIENT_NAME = "mcpcat";
6414
6414
  var CALLBACK_HOST = "127.0.0.1";
6415
6415
  var CALLBACK_PATH = "/callback";
6416
6416
  var REQUEST_TIMEOUT_MS = 15e3;
6417
- var REFRESH_SKEW_MS = 3e4;
6417
+ var MAX_REFRESH_SKEW_MS = 3e4;
6418
+ function computeSkew(state) {
6419
+ const expiresIn = state?.tokens?.expires_in;
6420
+ if (typeof expiresIn !== "number" || expiresIn <= 0) return MAX_REFRESH_SKEW_MS;
6421
+ const ttlMs = expiresIn * 1e3;
6422
+ return Math.min(MAX_REFRESH_SKEW_MS, Math.floor(ttlMs / 2));
6423
+ }
6418
6424
  var FALLBACK_REDIRECT_URL = `http://${CALLBACK_HOST}${CALLBACK_PATH}`;
6419
6425
  function timedFetch(fetchImpl) {
6420
6426
  return (input, init) => {
@@ -6562,7 +6568,8 @@ function createPiAuthProvider(options) {
6562
6568
  cachedToken = state?.tokens?.access_token;
6563
6569
  registerCredential(cachedToken);
6564
6570
  if (state?.tokens?.refresh_token) registerCredential(state.tokens.refresh_token);
6565
- const expired = state?.tokensExpireAt !== void 0 && state.tokensExpireAt - REFRESH_SKEW_MS <= Date.now();
6571
+ const skew = computeSkew(state);
6572
+ const expired = state?.tokensExpireAt !== void 0 && state.tokensExpireAt - skew <= Date.now();
6566
6573
  if (expired && state?.tokens?.refresh_token) {
6567
6574
  await refresh(cachedToken).catch(() => void 0);
6568
6575
  const refreshedState = await store.load();
@@ -11417,7 +11424,8 @@ ${stderr}` : stderr : stdout;
11417
11424
  return {
11418
11425
  content: [{ type: "text", text }],
11419
11426
  isError: result.exitCode !== 0,
11420
- structuredContent: { stdout, stderr, exitCode: result.exitCode }
11427
+ structuredContent: { stdout, stderr, exitCode: result.exitCode },
11428
+ ...result.meta ? { _meta: result.meta } : {}
11421
11429
  };
11422
11430
  }
11423
11431
  function initializeResult(params, serverName, serverVersion) {
@@ -11717,6 +11725,219 @@ function createProxyHandler(options) {
11717
11725
  return { handle, shutdown };
11718
11726
  }
11719
11727
 
11728
+ // src/serve/webmcp.ts
11729
+ function buildBaseArgs(options) {
11730
+ const args = [];
11731
+ if (options.cdp) {
11732
+ args.push("--cdp", options.cdp);
11733
+ }
11734
+ if (options.tab) {
11735
+ args.push("--tab", options.tab);
11736
+ }
11737
+ return args;
11738
+ }
11739
+ function generateNonce() {
11740
+ const bytes = new Uint8Array(8);
11741
+ crypto.getRandomValues(bytes);
11742
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
11743
+ }
11744
+ function wrapUntrusted(content) {
11745
+ const nonce = generateNonce();
11746
+ const openTag = `<<untrusted-page-content id=${nonce}>>`;
11747
+ const closeTag = `<<end id=${nonce}>>`;
11748
+ const neutralised = content.replaceAll("<<untrusted-page-content", "< <untrusted-page-content");
11749
+ return `${openTag}
11750
+ ${neutralised}
11751
+ ${closeTag}`;
11752
+ }
11753
+ async function runProcess(platform2, argv, stdin, timeoutMs) {
11754
+ let proc;
11755
+ try {
11756
+ proc = await platform2.spawn({ argv });
11757
+ } catch (err) {
11758
+ const code = err.code;
11759
+ if (code === "ENOENT" || code === "EACCES") {
11760
+ return { stdout: "", stderr: `command not found: ${argv[0]}`, exitCode: -1 };
11761
+ }
11762
+ throw err;
11763
+ }
11764
+ if (stdin !== void 0) {
11765
+ const writer = proc.stdin.getWriter();
11766
+ await writer.write(new TextEncoder().encode(stdin));
11767
+ await writer.close();
11768
+ } else {
11769
+ try {
11770
+ await proc.stdin.getWriter().close();
11771
+ } catch {
11772
+ }
11773
+ }
11774
+ const stdoutChunks = [];
11775
+ const stderrChunks = [];
11776
+ const readStream3 = async (stream, into) => {
11777
+ const r = stream.getReader();
11778
+ try {
11779
+ for (; ; ) {
11780
+ const { done, value } = await r.read();
11781
+ if (done) break;
11782
+ into.push(value);
11783
+ }
11784
+ } finally {
11785
+ r.releaseLock();
11786
+ }
11787
+ };
11788
+ const timer = new Promise(
11789
+ (resolve2) => setTimeout(() => resolve2("timeout"), timeoutMs ?? 3e4)
11790
+ );
11791
+ const work = Promise.all([
11792
+ readStream3(proc.stdout, stdoutChunks),
11793
+ readStream3(proc.stderr, stderrChunks),
11794
+ proc.wait()
11795
+ ]);
11796
+ const result = await Promise.race([work, timer]);
11797
+ if (result === "timeout") {
11798
+ await proc.kill("SIGTERM");
11799
+ return { stdout: "", stderr: "timed out", exitCode: 124 };
11800
+ }
11801
+ const [, , exitResult] = result;
11802
+ const dec = new TextDecoder();
11803
+ const concat2 = (chunks) => {
11804
+ const total = chunks.reduce((s, c) => s + c.length, 0);
11805
+ const out = new Uint8Array(total);
11806
+ let offset = 0;
11807
+ for (const c of chunks) {
11808
+ out.set(c, offset);
11809
+ offset += c.length;
11810
+ }
11811
+ return out;
11812
+ };
11813
+ return {
11814
+ stdout: dec.decode(concat2(stdoutChunks)),
11815
+ stderr: dec.decode(concat2(stderrChunks)),
11816
+ exitCode: exitResult.status
11817
+ };
11818
+ }
11819
+ function createWebMcpHandler(platform2, options) {
11820
+ let cache;
11821
+ const baseArgs = buildBaseArgs(options);
11822
+ async function listRaw() {
11823
+ const now = Date.now();
11824
+ if (cache && now < cache.expiresAt) {
11825
+ return { tools: cache.tools };
11826
+ }
11827
+ const argv = [options.playwrightCli, "webmcp-list", "--json", ...baseArgs];
11828
+ const result = await runProcess(platform2, argv, void 0, options.callTimeoutMs);
11829
+ if (result.exitCode === -1) {
11830
+ return { tools: [], error: result.stderr, notFound: true };
11831
+ }
11832
+ if (result.exitCode !== 0) {
11833
+ return { tools: [], error: result.stderr || `webmcp-list exited ${result.exitCode}` };
11834
+ }
11835
+ let parsed;
11836
+ try {
11837
+ parsed = JSON.parse(result.stdout);
11838
+ } catch {
11839
+ return { tools: [], error: "webmcp-list returned invalid JSON" };
11840
+ }
11841
+ if (!Array.isArray(parsed)) {
11842
+ return { tools: [], error: "webmcp-list did not return an array" };
11843
+ }
11844
+ const tools = parsed;
11845
+ cache = { tools, expiresAt: now + options.ttlMs };
11846
+ return { tools };
11847
+ }
11848
+ function mapDescriptionAnnotations(ann) {
11849
+ const mapped = { openWorldHint: true };
11850
+ if (!ann) return mapped;
11851
+ if (ann.readOnlyHint !== void 0) mapped.readOnlyHint = ann.readOnlyHint;
11852
+ if (ann.consequentialHint) mapped.destructiveHint = true;
11853
+ return mapped;
11854
+ }
11855
+ function originPrefix(tools) {
11856
+ const origins = new Set(tools.map((t) => t.origin));
11857
+ return origins.size > 1;
11858
+ }
11859
+ function hostFromOrigin(origin) {
11860
+ try {
11861
+ return new URL(origin).host;
11862
+ } catch {
11863
+ return origin;
11864
+ }
11865
+ }
11866
+ function buildDescription(tool) {
11867
+ const parts = [];
11868
+ if (tool.description) parts.push(tool.description);
11869
+ parts.push(`[origin: ${tool.origin}]`);
11870
+ return parts.join("\n");
11871
+ }
11872
+ function buildToolName(tool, prefix) {
11873
+ const base = tool.name;
11874
+ if (!prefix) return base;
11875
+ const host = hostFromOrigin(tool.origin).replace(/[^a-zA-Z0-9_]/g, "_");
11876
+ return `${host}__${base}`;
11877
+ }
11878
+ async function getRpcTools() {
11879
+ const { tools, error, notFound } = await listRaw();
11880
+ if (notFound) return null;
11881
+ if (error) return [];
11882
+ const needPrefix = originPrefix(tools);
11883
+ const rpcTools = [];
11884
+ for (const tool of tools) {
11885
+ if (tool.annotations?.consequentialHint && !options.allowConsequential) {
11886
+ continue;
11887
+ }
11888
+ const mapped = mapDescriptionAnnotations(tool.annotations);
11889
+ rpcTools.push({
11890
+ name: buildToolName(tool, needPrefix),
11891
+ description: buildDescription(tool),
11892
+ inputSchema: tool.inputSchema,
11893
+ annotations: mapped
11894
+ });
11895
+ }
11896
+ return rpcTools;
11897
+ }
11898
+ async function callTool(name, args) {
11899
+ const { tools, error } = await listRaw();
11900
+ if (error) {
11901
+ return { stdout: "", stderr: error, exitCode: 2 };
11902
+ }
11903
+ const needPrefix = originPrefix(tools);
11904
+ const tool = tools.find((t) => buildToolName(t, needPrefix) === name);
11905
+ if (!tool) {
11906
+ return { stdout: "", stderr: `unknown tool: ${name}`, exitCode: 2 };
11907
+ }
11908
+ const argv = [options.playwrightCli, "webmcp-call", tool.name, "-", ...baseArgs];
11909
+ if (options.allowConsequential) {
11910
+ argv.push("--allow-consequential");
11911
+ }
11912
+ const stdinData = JSON.stringify(args ?? {});
11913
+ const result = await runProcess(platform2, argv, stdinData, options.callTimeoutMs);
11914
+ if (result.exitCode === -1) {
11915
+ throw new Error(result.stderr || "playwright-cli not found");
11916
+ }
11917
+ if (result.exitCode === 2) {
11918
+ throw new Error(result.stderr || "webmcp-call error");
11919
+ }
11920
+ const meta = {
11921
+ "io.mcpcat/untrusted": true,
11922
+ origin: tool.origin
11923
+ };
11924
+ if (result.exitCode === 1) {
11925
+ return {
11926
+ stdout: "",
11927
+ stderr: wrapUntrusted(result.stderr || result.stdout),
11928
+ exitCode: 1,
11929
+ meta
11930
+ };
11931
+ }
11932
+ const text = wrapUntrusted(result.stdout);
11933
+ return { stdout: text, stderr: result.stderr, exitCode: 0, meta };
11934
+ }
11935
+ function invalidateCache() {
11936
+ cache = void 0;
11937
+ }
11938
+ return { getRpcTools, callTool, invalidateCache };
11939
+ }
11940
+
11720
11941
  // src/serve/runtime.ts
11721
11942
  var CALL_TIMEOUT_MS = 12e4;
11722
11943
  var MAX_OUTPUT_DEFAULT = 1048576;
@@ -11751,6 +11972,10 @@ function parseSingleFlag(args, i, opts) {
11751
11972
  opts.json = true;
11752
11973
  } else if (arg === "--no-auth") {
11753
11974
  opts.noAuth = true;
11975
+ } else if (arg === "--webmcp") {
11976
+ opts.webmcp = true;
11977
+ } else if (arg === "--allow-consequential") {
11978
+ opts.webmcpAllowConsequential = true;
11754
11979
  } else if (!arg.startsWith("-")) {
11755
11980
  opts.commands.push(parseCommandTarget(arg));
11756
11981
  } else {
@@ -11792,6 +12017,18 @@ function parseValueFlag(args, i, opts) {
11792
12017
  case "--cwd":
11793
12018
  opts.cwd = args[++i];
11794
12019
  break;
12020
+ case "--tab":
12021
+ opts.webmcpTab = args[++i];
12022
+ break;
12023
+ case "--webmcp-ttl":
12024
+ opts.webmcpTtl = Number(args[++i]) * 1e3;
12025
+ break;
12026
+ case "--playwright-cli":
12027
+ opts.webmcpPlaywrightCli = args[++i];
12028
+ break;
12029
+ case "--cdp":
12030
+ opts.webmcpCdp = args[++i];
12031
+ break;
11795
12032
  case "-H":
11796
12033
  case "--header": {
11797
12034
  const hdr = args[++i];
@@ -11824,7 +12061,13 @@ function parseServeFlags(args) {
11824
12061
  json: false,
11825
12062
  commands: [],
11826
12063
  proxy: void 0,
11827
- proxyHeaders: []
12064
+ proxyHeaders: [],
12065
+ webmcp: false,
12066
+ webmcpTab: void 0,
12067
+ webmcpTtl: 5e3,
12068
+ webmcpAllowConsequential: false,
12069
+ webmcpPlaywrightCli: void 0,
12070
+ webmcpCdp: void 0
11828
12071
  };
11829
12072
  for (let i = 0; i < args.length; i++) {
11830
12073
  i = parseSingleFlag(args, i, opts);
@@ -12206,25 +12449,25 @@ async function discoverWithSignal(platform2, commands, childEnv, cwd) {
12206
12449
  if (earlyShutdown) return { exitCode: 143, inflightDone };
12207
12450
  return { clis };
12208
12451
  }
12209
- async function runServe(platform2, options, version) {
12210
- let resolvedBearer;
12211
- let generatedBearer = false;
12452
+ async function resolveBearer(platform2, options) {
12212
12453
  if (options._resolvedBearer) {
12213
- resolvedBearer = options._resolvedBearer;
12214
- } else if (options.bearer) {
12454
+ return { resolved: options._resolvedBearer, generated: false };
12455
+ }
12456
+ if (options.bearer) {
12215
12457
  const expanded = expandBearerToken(options.bearer, platform2.env);
12216
12458
  if (!expanded) {
12217
12459
  await platform2.write(
12218
12460
  2,
12219
12461
  "mcpcat serve: --bearer requires a ${NAME} variable reference, not a literal token. Use --bearer '${MY_TOKEN}' and set MY_TOKEN in the environment.\n"
12220
12462
  );
12221
- return 2;
12463
+ return { exitCode: 2 };
12222
12464
  }
12223
- resolvedBearer = expanded;
12224
- } else if (options.noAuth) {
12465
+ return { resolved: expanded, generated: false };
12466
+ }
12467
+ if (options.noAuth) {
12225
12468
  if (options.transport === "http" && !isLoopbackBind(options.host)) {
12226
12469
  await platform2.write(2, "mcpcat serve: --no-auth is only allowed on loopback\n");
12227
- return 2;
12470
+ return { exitCode: 2 };
12228
12471
  }
12229
12472
  if (options.transport === "http") {
12230
12473
  await platform2.write(
@@ -12232,21 +12475,30 @@ async function runServe(platform2, options, version) {
12232
12475
  "mcpcat serve: warning: running without authentication on loopback\n"
12233
12476
  );
12234
12477
  }
12235
- resolvedBearer = void 0;
12236
- } else if (options.transport === "http") {
12478
+ return { resolved: void 0, generated: false };
12479
+ }
12480
+ if (options.transport === "http") {
12237
12481
  if (platform2.kind === "kernel") {
12238
12482
  await platform2.write(
12239
12483
  2,
12240
12484
  "mcpcat serve: kernel HTTP serve requires --bearer '${VAR}' or --no-auth\n"
12241
12485
  );
12242
- return 2;
12486
+ return { exitCode: 2 };
12243
12487
  }
12244
- resolvedBearer = generateToken();
12245
- generatedBearer = true;
12488
+ return { resolved: generateToken(), generated: true };
12246
12489
  }
12490
+ return { resolved: void 0, generated: false };
12491
+ }
12492
+ async function runServe(platform2, options, version) {
12493
+ const bearerResult = await resolveBearer(platform2, options);
12494
+ if ("exitCode" in bearerResult) return bearerResult.exitCode;
12495
+ const { resolved: resolvedBearer, generated: generatedBearer } = bearerResult;
12247
12496
  if (options.proxy) {
12248
12497
  return runProxyServe(platform2, options, version, resolvedBearer, generatedBearer);
12249
12498
  }
12499
+ if (options.webmcp) {
12500
+ return runWebMcpServe(platform2, options, version, resolvedBearer, generatedBearer);
12501
+ }
12250
12502
  const serveCwd = options.cwd ?? platform2.cwd;
12251
12503
  const childEnv = buildChildEnv(platform2, options.envAllow);
12252
12504
  const discovery = await discoverWithSignal(platform2, options.commands, childEnv, serveCwd);
@@ -12380,6 +12632,233 @@ async function buildProxyConnectOpts(platform2, options, version) {
12380
12632
  }
12381
12633
  return { connectOpts, hiddenValues };
12382
12634
  }
12635
+ function resolvePlaywrightCli(platform2, explicit) {
12636
+ if (explicit) return explicit;
12637
+ if (platform2.kind === "kernel") return "playwright-cli";
12638
+ const pathEnv = platform2.env.PATH ?? "";
12639
+ const sep = pathEnv.includes(";") ? ";" : ":";
12640
+ for (const dir of pathEnv.split(sep)) {
12641
+ if (!dir) continue;
12642
+ return "playwright-cli";
12643
+ }
12644
+ return void 0;
12645
+ }
12646
+ async function runWebMcpServe(platform2, options, version, resolvedBearer, generatedBearer) {
12647
+ const cliPath = resolvePlaywrightCli(platform2, options.webmcpPlaywrightCli);
12648
+ if (!cliPath) {
12649
+ await platform2.write(
12650
+ 2,
12651
+ "mcpcat serve --webmcp: playwright-cli not found. Install it with: curl -fsSL https://get.sliccy.ai/playwright-cli | sh\n"
12652
+ );
12653
+ return 3;
12654
+ }
12655
+ const cdp = options.webmcpCdp ?? platform2.env.SLICC_CDP_URL;
12656
+ const handler = createWebMcpHandler(platform2, {
12657
+ playwrightCli: cliPath,
12658
+ tab: options.webmcpTab,
12659
+ cdp,
12660
+ allowConsequential: options.webmcpAllowConsequential,
12661
+ ttlMs: options.webmcpTtl,
12662
+ callTimeoutMs: options.callTimeout,
12663
+ maxOutput: options.maxOutput
12664
+ });
12665
+ const probe = await handler.getRpcTools();
12666
+ if (probe === null) {
12667
+ await platform2.write(
12668
+ 2,
12669
+ "mcpcat serve --webmcp: playwright-cli not found. Install it with: curl -fsSL https://get.sliccy.ai/playwright-cli | sh\n"
12670
+ );
12671
+ return 3;
12672
+ }
12673
+ const serverName = "mcpcat-webmcp";
12674
+ const getTools = async () => await handler.getRpcTools() ?? [];
12675
+ const callTool = (name, args) => handler.callTool(name, args);
12676
+ if (options.transport === "stdio") {
12677
+ return runWebMcpStdio(platform2, getTools, callTool, serverName, version);
12678
+ }
12679
+ return runWebMcpHttp(
12680
+ platform2,
12681
+ options,
12682
+ getTools,
12683
+ callTool,
12684
+ serverName,
12685
+ version,
12686
+ resolvedBearer,
12687
+ generatedBearer
12688
+ );
12689
+ }
12690
+ async function runWebMcpStdio(platform2, getTools, callTool, serverName, version) {
12691
+ const reader = platform2.stdin.getReader();
12692
+ let buffer = "";
12693
+ const dec = new TextDecoder();
12694
+ const enc = new TextEncoder();
12695
+ try {
12696
+ for (; ; ) {
12697
+ const { done, value } = await reader.read();
12698
+ if (done) break;
12699
+ buffer += dec.decode(value, { stream: true });
12700
+ let nlIdx;
12701
+ while ((nlIdx = buffer.indexOf("\n")) >= 0) {
12702
+ const line = buffer.slice(0, nlIdx).trim();
12703
+ buffer = buffer.slice(nlIdx + 1);
12704
+ if (!line) continue;
12705
+ const tools = await getTools();
12706
+ const outcome = await handleJsonRpc(line, tools, callTool, serverName, version);
12707
+ if (outcome.status === 202) continue;
12708
+ const writer = platform2.stdout.getWriter();
12709
+ await writer.write(enc.encode(`${outcome.body}
12710
+ `));
12711
+ writer.releaseLock();
12712
+ }
12713
+ }
12714
+ } finally {
12715
+ reader.releaseLock();
12716
+ }
12717
+ return 0;
12718
+ }
12719
+ async function runWebMcpHttp(platform2, options, getTools, callTool, serverName, version, resolvedBearer, generatedBearer) {
12720
+ if (!platform2.listen) {
12721
+ await platform2.write(2, "mcpcat serve --webmcp: HTTP transport requires platform.listen\n");
12722
+ return 3;
12723
+ }
12724
+ const boundLoopback = isLoopbackBind(options.host);
12725
+ if (!boundLoopback && !resolvedBearer) {
12726
+ await platform2.write(
12727
+ 2,
12728
+ "mcpcat serve --webmcp: --bearer is required when binding off loopback\n"
12729
+ );
12730
+ return 2;
12731
+ }
12732
+ const allowedOrigins = new Set(options.allowOrigins);
12733
+ const listener = await platform2.listen({ host: options.host, port: options.port });
12734
+ const actualPort = listener.port;
12735
+ const serverOrigins = buildServerOrigins(options.host, actualPort);
12736
+ const allowedHosts = buildAllowedHosts(options.host, actualPort, options.allowHosts);
12737
+ const listenUrl = `http://${options.host}:${actualPort}/mcp`;
12738
+ let tokenFilePath;
12739
+ if (generatedBearer && resolvedBearer) {
12740
+ tokenFilePath = await publishBearer(
12741
+ platform2,
12742
+ resolvedBearer,
12743
+ actualPort,
12744
+ listenUrl,
12745
+ options.json
12746
+ );
12747
+ } else {
12748
+ if (options.json) {
12749
+ await platform2.write(1, `${JSON.stringify({ url: listenUrl })}
12750
+ `);
12751
+ }
12752
+ }
12753
+ await platform2.write(2, `mcpcat serve --webmcp: listening on ${listenUrl}
12754
+ `);
12755
+ if (options.onListening) {
12756
+ await options.onListening(actualPort);
12757
+ }
12758
+ let shutdownRequested = false;
12759
+ const shutdown = () => {
12760
+ if (shutdownRequested) return;
12761
+ shutdownRequested = true;
12762
+ listener.close();
12763
+ if (tokenFilePath) {
12764
+ platform2.fs.unlink?.(tokenFilePath).catch(() => {
12765
+ });
12766
+ }
12767
+ };
12768
+ if (platform2.onSignal) {
12769
+ platform2.onSignal("SIGINT", shutdown);
12770
+ platform2.onSignal("SIGTERM", shutdown);
12771
+ platform2.onSignal("SIGHUP", shutdown);
12772
+ }
12773
+ const handler = async (request) => {
12774
+ if (request.target !== "/mcp") {
12775
+ return {
12776
+ status: 404,
12777
+ reason: "Not Found",
12778
+ headers: { "content-type": "text/plain" },
12779
+ body: "Not Found"
12780
+ };
12781
+ }
12782
+ if (request.method !== "POST") {
12783
+ return {
12784
+ status: 405,
12785
+ reason: "Method Not Allowed",
12786
+ headers: { "content-type": "text/plain", allow: "POST" },
12787
+ body: "Method Not Allowed"
12788
+ };
12789
+ }
12790
+ if (!checkContentType(request.headers["content-type"])) {
12791
+ return {
12792
+ status: 415,
12793
+ reason: "Unsupported Media Type",
12794
+ headers: { "content-type": "text/plain" },
12795
+ body: "Content-Type must be application/json"
12796
+ };
12797
+ }
12798
+ if (!checkAcceptHeader(request.headers.accept)) {
12799
+ return {
12800
+ status: 406,
12801
+ reason: "Not Acceptable",
12802
+ headers: { "content-type": "text/plain" },
12803
+ body: "Accept must include application/json or text/event-stream"
12804
+ };
12805
+ }
12806
+ const originCheck = checkOrigin(request.headers.origin, serverOrigins, allowedOrigins);
12807
+ if (!originCheck.ok) {
12808
+ return {
12809
+ status: 403,
12810
+ reason: "Forbidden",
12811
+ headers: { "content-type": "text/plain" },
12812
+ body: originCheck.reason ?? "Forbidden"
12813
+ };
12814
+ }
12815
+ const hostCheck = checkHost(request.headers.host, allowedHosts);
12816
+ if (!hostCheck.ok) {
12817
+ return {
12818
+ status: 403,
12819
+ reason: "Forbidden",
12820
+ headers: { "content-type": "text/plain" },
12821
+ body: hostCheck.reason ?? "Forbidden"
12822
+ };
12823
+ }
12824
+ if (resolvedBearer) {
12825
+ const authHeader = request.headers.authorization;
12826
+ const provided = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : "";
12827
+ if (!provided || !constantTimeEqual(provided, resolvedBearer)) {
12828
+ return {
12829
+ status: 401,
12830
+ reason: "Unauthorized",
12831
+ headers: { "content-type": "text/plain" },
12832
+ body: "Unauthorized"
12833
+ };
12834
+ }
12835
+ }
12836
+ const tools = await getTools();
12837
+ const outcome = await handleJsonRpc(request.body, tools, callTool, serverName, version);
12838
+ return {
12839
+ status: outcome.status,
12840
+ reason: outcome.status === 200 ? "OK" : "Accepted",
12841
+ headers: { "content-type": outcome.contentType },
12842
+ body: outcome.body
12843
+ };
12844
+ };
12845
+ try {
12846
+ for await (const connection of listener) {
12847
+ if (shutdownRequested) break;
12848
+ const conn = connection;
12849
+ handleConnection(conn.readable, conn.writable, handler).catch(() => {
12850
+ }).finally(() => {
12851
+ try {
12852
+ conn.close();
12853
+ } catch {
12854
+ }
12855
+ });
12856
+ }
12857
+ } catch {
12858
+ if (!shutdownRequested) throw new Error("listener closed unexpectedly");
12859
+ }
12860
+ return 0;
12861
+ }
12383
12862
  async function runProxyServe(platform2, options, version, resolvedBearer, generatedBearer) {
12384
12863
  const built = await buildProxyConnectOpts(platform2, options, version);
12385
12864
  if ("error" in built) {
@@ -14958,7 +15437,7 @@ async function dispatchDiscoveryVerb(platform2, parsed, args) {
14958
15437
  async function dispatchServeVerb(platform2, args, version) {
14959
15438
  const serveArgs = args.slice(args.indexOf("serve") + 1);
14960
15439
  const serveOptions = parseServeFlags(serveArgs);
14961
- if (!serveOptions.proxy && serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
15440
+ if (!serveOptions.proxy && !serveOptions.webmcp && serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
14962
15441
  await platform2.write(2, "mcpcat serve: no commands or --tool templates given\n");
14963
15442
  return 2;
14964
15443
  }
@@ -15329,7 +15808,7 @@ async function handleInputRequired(platform2, conn, err, respondData, json) {
15329
15808
  `);
15330
15809
  return EXIT_INPUT_REQUIRED;
15331
15810
  }
15332
- const isTty = platform2.isatty(0) && platform2.isatty(2);
15811
+ const isTty = platform2.kind === "host" && platform2.isatty(0) && platform2.isatty(2);
15333
15812
  const elicitResult = await handleElicitation(platform2, inputReq, isTty, respondData);
15334
15813
  if ("exitCode" in elicitResult) {
15335
15814
  await platform2.write(2, `${err.requestJson}
@@ -15834,7 +16313,7 @@ async function main(ctx) {
15834
16313
  packageDir: "/node_modules/@ai-ecoverse/mcpcat"
15835
16314
  });
15836
16315
  const args = ctx.argv.slice(1);
15837
- return run(platform2, "0.14.6", args);
16316
+ return run(platform2, "0.15.0", args);
15838
16317
  }
15839
16318
  export {
15840
16319
  main as default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/mcpcat",
3
- "version": "0.14.6",
3
+ "version": "0.15.0",
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.82.0",
59
+ "@ai-ecoverse/slicc-kernel": "1.83.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",