@ai-ecoverse/mcpcat 0.14.7 → 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.
@@ -11803,7 +11803,8 @@ ${stderr}` : stderr : stdout;
11803
11803
  return {
11804
11804
  content: [{ type: "text", text }],
11805
11805
  isError: result.exitCode !== 0,
11806
- structuredContent: { stdout, stderr, exitCode: result.exitCode }
11806
+ structuredContent: { stdout, stderr, exitCode: result.exitCode },
11807
+ ...result.meta ? { _meta: result.meta } : {}
11807
11808
  };
11808
11809
  }
11809
11810
  function initializeResult(params, serverName, serverVersion) {
@@ -12103,6 +12104,219 @@ function createProxyHandler(options) {
12103
12104
  return { handle, shutdown };
12104
12105
  }
12105
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
+
12106
12320
  // src/serve/runtime.ts
12107
12321
  var CALL_TIMEOUT_MS = 12e4;
12108
12322
  var MAX_OUTPUT_DEFAULT = 1048576;
@@ -12137,6 +12351,10 @@ function parseSingleFlag(args2, i, opts) {
12137
12351
  opts.json = true;
12138
12352
  } else if (arg === "--no-auth") {
12139
12353
  opts.noAuth = true;
12354
+ } else if (arg === "--webmcp") {
12355
+ opts.webmcp = true;
12356
+ } else if (arg === "--allow-consequential") {
12357
+ opts.webmcpAllowConsequential = true;
12140
12358
  } else if (!arg.startsWith("-")) {
12141
12359
  opts.commands.push(parseCommandTarget(arg));
12142
12360
  } else {
@@ -12178,6 +12396,18 @@ function parseValueFlag(args2, i, opts) {
12178
12396
  case "--cwd":
12179
12397
  opts.cwd = args2[++i];
12180
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;
12181
12411
  case "-H":
12182
12412
  case "--header": {
12183
12413
  const hdr = args2[++i];
@@ -12210,7 +12440,13 @@ function parseServeFlags(args2) {
12210
12440
  json: false,
12211
12441
  commands: [],
12212
12442
  proxy: void 0,
12213
- proxyHeaders: []
12443
+ proxyHeaders: [],
12444
+ webmcp: false,
12445
+ webmcpTab: void 0,
12446
+ webmcpTtl: 5e3,
12447
+ webmcpAllowConsequential: false,
12448
+ webmcpPlaywrightCli: void 0,
12449
+ webmcpCdp: void 0
12214
12450
  };
12215
12451
  for (let i = 0; i < args2.length; i++) {
12216
12452
  i = parseSingleFlag(args2, i, opts);
@@ -12592,25 +12828,25 @@ async function discoverWithSignal(platform2, commands, childEnv, cwd) {
12592
12828
  if (earlyShutdown) return { exitCode: 143, inflightDone };
12593
12829
  return { clis };
12594
12830
  }
12595
- async function runServe(platform2, options, version) {
12596
- let resolvedBearer;
12597
- let generatedBearer = false;
12831
+ async function resolveBearer(platform2, options) {
12598
12832
  if (options._resolvedBearer) {
12599
- resolvedBearer = options._resolvedBearer;
12600
- } else if (options.bearer) {
12833
+ return { resolved: options._resolvedBearer, generated: false };
12834
+ }
12835
+ if (options.bearer) {
12601
12836
  const expanded = expandBearerToken(options.bearer, platform2.env);
12602
12837
  if (!expanded) {
12603
12838
  await platform2.write(
12604
12839
  2,
12605
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"
12606
12841
  );
12607
- return 2;
12842
+ return { exitCode: 2 };
12608
12843
  }
12609
- resolvedBearer = expanded;
12610
- } else if (options.noAuth) {
12844
+ return { resolved: expanded, generated: false };
12845
+ }
12846
+ if (options.noAuth) {
12611
12847
  if (options.transport === "http" && !isLoopbackBind(options.host)) {
12612
12848
  await platform2.write(2, "mcpcat serve: --no-auth is only allowed on loopback\n");
12613
- return 2;
12849
+ return { exitCode: 2 };
12614
12850
  }
12615
12851
  if (options.transport === "http") {
12616
12852
  await platform2.write(
@@ -12618,21 +12854,30 @@ async function runServe(platform2, options, version) {
12618
12854
  "mcpcat serve: warning: running without authentication on loopback\n"
12619
12855
  );
12620
12856
  }
12621
- resolvedBearer = void 0;
12622
- } else if (options.transport === "http") {
12857
+ return { resolved: void 0, generated: false };
12858
+ }
12859
+ if (options.transport === "http") {
12623
12860
  if (platform2.kind === "kernel") {
12624
12861
  await platform2.write(
12625
12862
  2,
12626
12863
  "mcpcat serve: kernel HTTP serve requires --bearer '${VAR}' or --no-auth\n"
12627
12864
  );
12628
- return 2;
12865
+ return { exitCode: 2 };
12629
12866
  }
12630
- resolvedBearer = generateToken();
12631
- generatedBearer = true;
12867
+ return { resolved: generateToken(), generated: true };
12632
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;
12633
12875
  if (options.proxy) {
12634
12876
  return runProxyServe(platform2, options, version, resolvedBearer, generatedBearer);
12635
12877
  }
12878
+ if (options.webmcp) {
12879
+ return runWebMcpServe(platform2, options, version, resolvedBearer, generatedBearer);
12880
+ }
12636
12881
  const serveCwd = options.cwd ?? platform2.cwd;
12637
12882
  const childEnv = buildChildEnv(platform2, options.envAllow);
12638
12883
  const discovery = await discoverWithSignal(platform2, options.commands, childEnv, serveCwd);
@@ -12766,6 +13011,233 @@ async function buildProxyConnectOpts(platform2, options, version) {
12766
13011
  }
12767
13012
  return { connectOpts, hiddenValues };
12768
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
+ }
12769
13241
  async function runProxyServe(platform2, options, version, resolvedBearer, generatedBearer) {
12770
13242
  const built = await buildProxyConnectOpts(platform2, options, version);
12771
13243
  if ("error" in built) {
@@ -15344,7 +15816,7 @@ async function dispatchDiscoveryVerb(platform2, parsed, args2) {
15344
15816
  async function dispatchServeVerb(platform2, args2, version) {
15345
15817
  const serveArgs = args2.slice(args2.indexOf("serve") + 1);
15346
15818
  const serveOptions = parseServeFlags(serveArgs);
15347
- 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) {
15348
15820
  await platform2.write(2, "mcpcat serve: no commands or --tool templates given\n");
15349
15821
  return 2;
15350
15822
  }
@@ -16177,5 +16649,5 @@ try {
16177
16649
  });
16178
16650
  } catch {
16179
16651
  }
16180
- var code = await run(platform, "0.14.7", args);
16652
+ var code = await run(platform, "0.15.0", args);
16181
16653
  process.exit(code);
@@ -11424,7 +11424,8 @@ ${stderr}` : stderr : stdout;
11424
11424
  return {
11425
11425
  content: [{ type: "text", text }],
11426
11426
  isError: result.exitCode !== 0,
11427
- structuredContent: { stdout, stderr, exitCode: result.exitCode }
11427
+ structuredContent: { stdout, stderr, exitCode: result.exitCode },
11428
+ ...result.meta ? { _meta: result.meta } : {}
11428
11429
  };
11429
11430
  }
11430
11431
  function initializeResult(params, serverName, serverVersion) {
@@ -11724,6 +11725,219 @@ function createProxyHandler(options) {
11724
11725
  return { handle, shutdown };
11725
11726
  }
11726
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
+
11727
11941
  // src/serve/runtime.ts
11728
11942
  var CALL_TIMEOUT_MS = 12e4;
11729
11943
  var MAX_OUTPUT_DEFAULT = 1048576;
@@ -11758,6 +11972,10 @@ function parseSingleFlag(args, i, opts) {
11758
11972
  opts.json = true;
11759
11973
  } else if (arg === "--no-auth") {
11760
11974
  opts.noAuth = true;
11975
+ } else if (arg === "--webmcp") {
11976
+ opts.webmcp = true;
11977
+ } else if (arg === "--allow-consequential") {
11978
+ opts.webmcpAllowConsequential = true;
11761
11979
  } else if (!arg.startsWith("-")) {
11762
11980
  opts.commands.push(parseCommandTarget(arg));
11763
11981
  } else {
@@ -11799,6 +12017,18 @@ function parseValueFlag(args, i, opts) {
11799
12017
  case "--cwd":
11800
12018
  opts.cwd = args[++i];
11801
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;
11802
12032
  case "-H":
11803
12033
  case "--header": {
11804
12034
  const hdr = args[++i];
@@ -11831,7 +12061,13 @@ function parseServeFlags(args) {
11831
12061
  json: false,
11832
12062
  commands: [],
11833
12063
  proxy: void 0,
11834
- proxyHeaders: []
12064
+ proxyHeaders: [],
12065
+ webmcp: false,
12066
+ webmcpTab: void 0,
12067
+ webmcpTtl: 5e3,
12068
+ webmcpAllowConsequential: false,
12069
+ webmcpPlaywrightCli: void 0,
12070
+ webmcpCdp: void 0
11835
12071
  };
11836
12072
  for (let i = 0; i < args.length; i++) {
11837
12073
  i = parseSingleFlag(args, i, opts);
@@ -12213,25 +12449,25 @@ async function discoverWithSignal(platform2, commands, childEnv, cwd) {
12213
12449
  if (earlyShutdown) return { exitCode: 143, inflightDone };
12214
12450
  return { clis };
12215
12451
  }
12216
- async function runServe(platform2, options, version) {
12217
- let resolvedBearer;
12218
- let generatedBearer = false;
12452
+ async function resolveBearer(platform2, options) {
12219
12453
  if (options._resolvedBearer) {
12220
- resolvedBearer = options._resolvedBearer;
12221
- } else if (options.bearer) {
12454
+ return { resolved: options._resolvedBearer, generated: false };
12455
+ }
12456
+ if (options.bearer) {
12222
12457
  const expanded = expandBearerToken(options.bearer, platform2.env);
12223
12458
  if (!expanded) {
12224
12459
  await platform2.write(
12225
12460
  2,
12226
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"
12227
12462
  );
12228
- return 2;
12463
+ return { exitCode: 2 };
12229
12464
  }
12230
- resolvedBearer = expanded;
12231
- } else if (options.noAuth) {
12465
+ return { resolved: expanded, generated: false };
12466
+ }
12467
+ if (options.noAuth) {
12232
12468
  if (options.transport === "http" && !isLoopbackBind(options.host)) {
12233
12469
  await platform2.write(2, "mcpcat serve: --no-auth is only allowed on loopback\n");
12234
- return 2;
12470
+ return { exitCode: 2 };
12235
12471
  }
12236
12472
  if (options.transport === "http") {
12237
12473
  await platform2.write(
@@ -12239,21 +12475,30 @@ async function runServe(platform2, options, version) {
12239
12475
  "mcpcat serve: warning: running without authentication on loopback\n"
12240
12476
  );
12241
12477
  }
12242
- resolvedBearer = void 0;
12243
- } else if (options.transport === "http") {
12478
+ return { resolved: void 0, generated: false };
12479
+ }
12480
+ if (options.transport === "http") {
12244
12481
  if (platform2.kind === "kernel") {
12245
12482
  await platform2.write(
12246
12483
  2,
12247
12484
  "mcpcat serve: kernel HTTP serve requires --bearer '${VAR}' or --no-auth\n"
12248
12485
  );
12249
- return 2;
12486
+ return { exitCode: 2 };
12250
12487
  }
12251
- resolvedBearer = generateToken();
12252
- generatedBearer = true;
12488
+ return { resolved: generateToken(), generated: true };
12253
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;
12254
12496
  if (options.proxy) {
12255
12497
  return runProxyServe(platform2, options, version, resolvedBearer, generatedBearer);
12256
12498
  }
12499
+ if (options.webmcp) {
12500
+ return runWebMcpServe(platform2, options, version, resolvedBearer, generatedBearer);
12501
+ }
12257
12502
  const serveCwd = options.cwd ?? platform2.cwd;
12258
12503
  const childEnv = buildChildEnv(platform2, options.envAllow);
12259
12504
  const discovery = await discoverWithSignal(platform2, options.commands, childEnv, serveCwd);
@@ -12387,6 +12632,233 @@ async function buildProxyConnectOpts(platform2, options, version) {
12387
12632
  }
12388
12633
  return { connectOpts, hiddenValues };
12389
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
+ }
12390
12862
  async function runProxyServe(platform2, options, version, resolvedBearer, generatedBearer) {
12391
12863
  const built = await buildProxyConnectOpts(platform2, options, version);
12392
12864
  if ("error" in built) {
@@ -14965,7 +15437,7 @@ async function dispatchDiscoveryVerb(platform2, parsed, args) {
14965
15437
  async function dispatchServeVerb(platform2, args, version) {
14966
15438
  const serveArgs = args.slice(args.indexOf("serve") + 1);
14967
15439
  const serveOptions = parseServeFlags(serveArgs);
14968
- 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) {
14969
15441
  await platform2.write(2, "mcpcat serve: no commands or --tool templates given\n");
14970
15442
  return 2;
14971
15443
  }
@@ -15841,7 +16313,7 @@ async function main(ctx) {
15841
16313
  packageDir: "/node_modules/@ai-ecoverse/mcpcat"
15842
16314
  });
15843
16315
  const args = ctx.argv.slice(1);
15844
- return run(platform2, "0.14.7", args);
16316
+ return run(platform2, "0.15.0", args);
15845
16317
  }
15846
16318
  export {
15847
16319
  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.0",
4
4
  "description": "A curl for the Model Context Protocol",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",