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