@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.
- package/dist/host/mcpcat.mjs +490 -18
- package/dist/kernel/mcpcat.mjs +490 -18
- package/package.json +1 -1
package/dist/host/mcpcat.mjs
CHANGED
|
@@ -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
|
|
12596
|
-
let resolvedBearer;
|
|
12597
|
-
let generatedBearer = false;
|
|
12831
|
+
async function resolveBearer(platform2, options) {
|
|
12598
12832
|
if (options._resolvedBearer) {
|
|
12599
|
-
|
|
12600
|
-
}
|
|
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
|
-
|
|
12610
|
-
}
|
|
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
|
-
|
|
12622
|
-
}
|
|
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
|
-
|
|
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.
|
|
16652
|
+
var code = await run(platform, "0.15.0", args);
|
|
16181
16653
|
process.exit(code);
|
package/dist/kernel/mcpcat.mjs
CHANGED
|
@@ -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
|
|
12217
|
-
let resolvedBearer;
|
|
12218
|
-
let generatedBearer = false;
|
|
12452
|
+
async function resolveBearer(platform2, options) {
|
|
12219
12453
|
if (options._resolvedBearer) {
|
|
12220
|
-
|
|
12221
|
-
}
|
|
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
|
-
|
|
12231
|
-
}
|
|
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
|
-
|
|
12243
|
-
}
|
|
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
|
-
|
|
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.
|
|
16316
|
+
return run(platform2, "0.15.0", args);
|
|
15845
16317
|
}
|
|
15846
16318
|
export {
|
|
15847
16319
|
main as default
|