@wenathlan/extension 1.1.54 → 1.1.55
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/README.md +4 -4
- package/dist/approvalgate.d.ts +39 -0
- package/dist/approvalgate.d.ts.map +1 -0
- package/dist/clientauth.d.ts +114 -0
- package/dist/clientauth.d.ts.map +1 -0
- package/dist/httpstream.d.ts +92 -0
- package/dist/httpstream.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4663 -4319
- package/dist/index.js.map +4 -4
- package/dist/mcpserver.d.ts +19 -6
- package/dist/mcpserver.d.ts.map +1 -1
- package/dist/memory.d.ts +37 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +19 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +76 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/toolcatalog.d.ts +2 -2
- package/dist/types.d.ts +150 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +639 -147
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js.map +1 -1
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +2 -1
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.js +163 -2
- package/extension/dist/sidepanel.js.map +3 -3
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -2438,6 +2438,78 @@ var sessionmemory = class {
|
|
|
2438
2438
|
async listtoolcalls() {
|
|
2439
2439
|
return await this.adapter.get("mcptoolcalls") ?? [];
|
|
2440
2440
|
}
|
|
2441
|
+
/** Returns every stored session token of paired remote clients; the records carry only their tokenhash form so raw tokens never persist. */
|
|
2442
|
+
async getsessiontokens() {
|
|
2443
|
+
return await this.adapter.get("mcpsessiontokens") ?? [];
|
|
2444
|
+
}
|
|
2445
|
+
/** Replaces the stored session token set after one issue, revocation or expiry sweep. */
|
|
2446
|
+
async setsessiontokens(tokens) {
|
|
2447
|
+
return this.adapter.set("mcpsessiontokens", tokens);
|
|
2448
|
+
}
|
|
2449
|
+
/** Returns every stored pairing code with its single use state, newest first. */
|
|
2450
|
+
async getpairingcodes() {
|
|
2451
|
+
return await this.adapter.get("mcppairingcodes") ?? [];
|
|
2452
|
+
}
|
|
2453
|
+
/** Records one issued pairing code for the one time client pairing. */
|
|
2454
|
+
async addpairingcode(code) {
|
|
2455
|
+
return this.adapter.set("mcppairingcodes", [code, ...(await this.getpairingcodes()).filter((candidate) => candidate.code !== code.code)]);
|
|
2456
|
+
}
|
|
2457
|
+
/** Marks one pairing code used so it never pairs a second client; an unknown code stays untouched. */
|
|
2458
|
+
async usepairingcode(code, now) {
|
|
2459
|
+
await this.adapter.set("mcppairingcodes", (await this.getpairingcodes()).map((candidate) => candidate.code === code ? { ...candidate, usedat: now } : candidate));
|
|
2460
|
+
}
|
|
2461
|
+
/** Returns every client allowlist entry with its grant history. */
|
|
2462
|
+
async getallowlist() {
|
|
2463
|
+
return await this.adapter.get("mcpallowlist") ?? [];
|
|
2464
|
+
}
|
|
2465
|
+
/** Upserts one client allowlist entry by its fingerprint with the grant history riding the record. */
|
|
2466
|
+
async setallowlistentry(entry) {
|
|
2467
|
+
await this.adapter.set("mcpallowlist", [entry, ...(await this.getallowlist()).filter((candidate) => candidate.fingerprint !== entry.fingerprint)]);
|
|
2468
|
+
}
|
|
2469
|
+
/** Removes one client allowlist entry so its fingerprint stops passing the allowlist check. */
|
|
2470
|
+
async removeallowlistentry(fingerprint) {
|
|
2471
|
+
await this.adapter.set("mcpallowlist", (await this.getallowlist()).filter((entry) => entry.fingerprint !== fingerprint));
|
|
2472
|
+
}
|
|
2473
|
+
/** Returns every approval gate with its decision state — the pending and resolved gates of the approval view. */
|
|
2474
|
+
async listapprovals() {
|
|
2475
|
+
return await this.adapter.get("mcpapprovals") ?? [];
|
|
2476
|
+
}
|
|
2477
|
+
/** Records one raised approval gate or its resolved state, keyed by the gate id. */
|
|
2478
|
+
async setapproval(request) {
|
|
2479
|
+
await this.adapter.set("mcpapprovals", [request, ...(await this.listapprovals()).filter((candidate) => candidate.id !== request.id)]);
|
|
2480
|
+
}
|
|
2481
|
+
/** Records one approval execution — the decision, the actor, the time and the latency — beside its gate. */
|
|
2482
|
+
async addapprovalexec(exec) {
|
|
2483
|
+
return this.adapter.set("mcpapprovalexecs", [exec, ...await this.adapter.get("mcpapprovalexecs") ?? []]);
|
|
2484
|
+
}
|
|
2485
|
+
/** Returns every approval execution record, newest first. */
|
|
2486
|
+
async listapprovalexecs() {
|
|
2487
|
+
return await this.adapter.get("mcpapprovalexecs") ?? [];
|
|
2488
|
+
}
|
|
2489
|
+
/** Records one auth handshake event with its issued, verified or refused outcome. */
|
|
2490
|
+
async addauthhandshake(event) {
|
|
2491
|
+
return this.adapter.set("mcpauthhandshakes", [event, ...await this.adapter.get("mcpauthhandshakes") ?? []]);
|
|
2492
|
+
}
|
|
2493
|
+
/** Returns every auth handshake event with its outcome, newest first. */
|
|
2494
|
+
async listauthhandshakes() {
|
|
2495
|
+
return await this.adapter.get("mcpauthhandshakes") ?? [];
|
|
2496
|
+
}
|
|
2497
|
+
/** Returns every stored client identity with its fingerprint for allowlist matching. */
|
|
2498
|
+
async getclientidentities() {
|
|
2499
|
+
return await this.adapter.get("mcpidentities") ?? [];
|
|
2500
|
+
}
|
|
2501
|
+
/** Upserts one client identity by its fingerprint so the allowlist matches it. */
|
|
2502
|
+
async setclientidentity(identity) {
|
|
2503
|
+
await this.adapter.set("mcpidentities", [identity, ...(await this.getclientidentities()).filter((candidate) => candidate.fingerprint !== identity.fingerprint)]);
|
|
2504
|
+
}
|
|
2505
|
+
/** Returns every open and closed stream channel of the http stream transport. */
|
|
2506
|
+
async getstreamchannels() {
|
|
2507
|
+
return await this.adapter.get("mcpchannels") ?? [];
|
|
2508
|
+
}
|
|
2509
|
+
/** Replaces the stored stream channel set after one open, heartbeat or close sweep. */
|
|
2510
|
+
async setstreamchannels(channels) {
|
|
2511
|
+
return this.adapter.set("mcpchannels", channels);
|
|
2512
|
+
}
|
|
2441
2513
|
};
|
|
2442
2514
|
function mediakindof(record2) {
|
|
2443
2515
|
if ("pages" in record2) return "pdf";
|
|
@@ -2491,7 +2563,7 @@ function readtool(name, kind, description, inputs = {}) {
|
|
|
2491
2563
|
return { name, version: toolcatalogversion, description, inputschema: toolschemaof({ target: { type: "string", description: "Reviewed css selector the tool addresses." }, value: { type: "string", description: "Reviewed literal value the tool carries." }, options: { type: "object", description: "Reviewed json options of the wrapped action kind with the empty default.", default: {} }, ...inputs }), kind, risk: "read" };
|
|
2492
2564
|
}
|
|
2493
2565
|
function gatedtool(name, kind, risk, description, review) {
|
|
2494
|
-
return { name, version: toolcatalogversion, description, inputschema: toolschemaof({ stepid: { type: "string", description: "Id of the approved plan step this tool executes.", required: true } }), kind, risk, consentmeta: { review } };
|
|
2566
|
+
return { name, version: toolcatalogversion, description, inputschema: toolschemaof({ stepid: { type: "string", description: "Id of the approved plan step this tool executes.", required: true } }), kind, risk, consentmeta: { review, riskclass: risk, approvalrequired: true, originscope: "session" } };
|
|
2495
2567
|
}
|
|
2496
2568
|
function browserdomain() {
|
|
2497
2569
|
return {
|
|
@@ -2567,6 +2639,10 @@ function resolvetool(catalog, name) {
|
|
|
2567
2639
|
const matches = alltools(catalog).filter((tool) => tool.name.split(".")[1] === name);
|
|
2568
2640
|
return matches.length === 1 ? matches[0] : void 0;
|
|
2569
2641
|
}
|
|
2642
|
+
function namespaceof(name) {
|
|
2643
|
+
const head = name.split(".")[0];
|
|
2644
|
+
return toolnamespaces.includes(head) ? head : void 0;
|
|
2645
|
+
}
|
|
2570
2646
|
|
|
2571
2647
|
// socketbus.ts
|
|
2572
2648
|
var socketkinds = ["opensocket", "sendmessage", "waitmessage", "subscribesse", "longpoll"];
|
|
@@ -8276,6 +8352,8 @@ function serverenablementgate(config) {
|
|
|
8276
8352
|
if (typeof config.port !== "number" || !Number.isFinite(config.port) || config.port <= 0 || config.port > 65535) return { allowed: false, reason: "The http listener port must be a valid port number." };
|
|
8277
8353
|
if (config.framesize !== void 0 && (typeof config.framesize !== "number" || !Number.isFinite(config.framesize) || config.framesize <= 0)) return { allowed: false, reason: "The user configured frame size must stay a positive number with no code ceiling." };
|
|
8278
8354
|
if (config.queuedepth !== void 0 && (typeof config.queuedepth !== "number" || !Number.isFinite(config.queuedepth) || config.queuedepth <= 0)) return { allowed: false, reason: "The user configured queue depth must stay a positive number with no code ceiling." };
|
|
8355
|
+
const remote = remoteenablementgate(config);
|
|
8356
|
+
if (!remote.allowed) return remote;
|
|
8279
8357
|
return { allowed: true };
|
|
8280
8358
|
}
|
|
8281
8359
|
function tooldispatchgate(input) {
|
|
@@ -8292,6 +8370,53 @@ function tooldispatchgate(input) {
|
|
|
8292
8370
|
if (step.kind !== input.tool.kind) return { allowed: false, reason: `The tool call names the step ${input.stepid} whose kind ${String(step.kind)} does not match the ${input.tool.name} tool.` };
|
|
8293
8371
|
return { allowed: true };
|
|
8294
8372
|
}
|
|
8373
|
+
function allowlistentryvalid(entry, identities) {
|
|
8374
|
+
if (typeof entry.fingerprint !== "string" || entry.fingerprint.trim() === "") return { allowed: false, reason: "The allowlist entry needs the client fingerprint it grants." };
|
|
8375
|
+
if (!identities.some((identity) => identity.fingerprint === entry.fingerprint)) return { allowed: false, reason: `The allowlist entry ${entry.fingerprint} matches no known client identity.` };
|
|
8376
|
+
if (typeof entry.displayname !== "string" || entry.displayname.trim() === "") return { allowed: false, reason: `The allowlist entry ${entry.fingerprint} needs its display name.` };
|
|
8377
|
+
if (!Array.isArray(entry.namespaces) || entry.namespaces.length === 0) return { allowed: false, reason: `The allowlist entry ${entry.displayname} grants no tool namespace.` };
|
|
8378
|
+
if (!entry.namespaces.every((namespace) => toolnamespaces.includes(namespace))) return { allowed: false, reason: `The allowlist entry ${entry.displayname} grants an unreviewed namespace.` };
|
|
8379
|
+
return { allowed: true };
|
|
8380
|
+
}
|
|
8381
|
+
function tokenlifetimevalid(lifetime) {
|
|
8382
|
+
if (lifetime === void 0) return { allowed: true };
|
|
8383
|
+
if (typeof lifetime !== "number" || !Number.isFinite(lifetime) || lifetime <= 0) return { allowed: false, reason: "The token lifetime must stay a positive user value with no code ceiling." };
|
|
8384
|
+
return { allowed: true };
|
|
8385
|
+
}
|
|
8386
|
+
function remotetransporttls(config) {
|
|
8387
|
+
const bind = config.bind !== void 0 && config.bind.trim() !== "" ? config.bind.trim() : "127.0.0.1";
|
|
8388
|
+
const local = bind === "127.0.0.1" || bind === "localhost" || bind === "::1";
|
|
8389
|
+
const tls = config.remoteaccess?.tls ?? config.httpstream?.tls;
|
|
8390
|
+
if ((config.remoteaccess !== void 0 || !local) && (tls === void 0 || tls.mode === "off")) return { allowed: false, reason: `The ${config.remoteaccess !== void 0 ? "remote transport" : `bind ${bind}`} leaves localhost and every non localhost transport requires tls before any remote traffic.` };
|
|
8391
|
+
return { allowed: true };
|
|
8392
|
+
}
|
|
8393
|
+
function pairingreadinessgate(session, now) {
|
|
8394
|
+
if (!session || session.stoppedat || session.pausedat) return { allowed: false, reason: "The pairing flow needs the live browser session before any code issues." };
|
|
8395
|
+
if (session.expiresat <= now) return { allowed: false, reason: "The browser session has expired and the pairing flow is refused." };
|
|
8396
|
+
return { allowed: true };
|
|
8397
|
+
}
|
|
8398
|
+
function remoteenablementgate(config) {
|
|
8399
|
+
if (config.remoteaccess === void 0) return { allowed: true };
|
|
8400
|
+
if (config.remote !== true) return { allowed: false, reason: "The remote transport enablement is a sensitive user choice and needs the explicit remote review." };
|
|
8401
|
+
const tls = remotetransporttls(config);
|
|
8402
|
+
if (!tls.allowed) return tls;
|
|
8403
|
+
if (typeof config.remoteaccess.endpoint !== "string" || config.remoteaccess.endpoint.trim() === "") return { allowed: false, reason: "The remote access policy needs its user configured endpoint." };
|
|
8404
|
+
if (config.remoteaccess.maxclients !== void 0 && (typeof config.remoteaccess.maxclients !== "number" || !Number.isFinite(config.remoteaccess.maxclients) || config.remoteaccess.maxclients <= 0)) return { allowed: false, reason: "The user configured client ceiling must stay a positive value with no code ceiling." };
|
|
8405
|
+
const lifetime = tokenlifetimevalid(config.remoteaccess.tokenlifetimems);
|
|
8406
|
+
if (!lifetime.allowed) return lifetime;
|
|
8407
|
+
const timeout = approvaltimeoutvalid(config.remoteaccess.approvaltimeout);
|
|
8408
|
+
if (!timeout.allowed) return timeout;
|
|
8409
|
+
return { allowed: true };
|
|
8410
|
+
}
|
|
8411
|
+
function approvaltimeoutvalid(timeout) {
|
|
8412
|
+
if (timeout === void 0) return { allowed: true };
|
|
8413
|
+
if (typeof timeout.windowms !== "number" || !Number.isFinite(timeout.windowms) || timeout.windowms <= 0) return { allowed: false, reason: "The approval timeout must stay a positive user window with no code ceiling." };
|
|
8414
|
+
if (timeout.ontimeout !== "refuse") return { allowed: false, reason: "The documented disposition of an unanswered approval gate is refusal." };
|
|
8415
|
+
return { allowed: true };
|
|
8416
|
+
}
|
|
8417
|
+
function revocationgate() {
|
|
8418
|
+
return { allowed: true };
|
|
8419
|
+
}
|
|
8295
8420
|
|
|
8296
8421
|
// progress.ts
|
|
8297
8422
|
function emptyprogress(planid, now) {
|
|
@@ -8471,11 +8596,291 @@ function recordtoolcall(progress, planid, stepid, entry, now) {
|
|
|
8471
8596
|
}
|
|
8472
8597
|
|
|
8473
8598
|
// version.ts
|
|
8474
|
-
var packageversion = "1.1.
|
|
8599
|
+
var packageversion = "1.1.55";
|
|
8475
8600
|
|
|
8476
8601
|
// types.ts
|
|
8477
8602
|
var protocolversion = packageversion;
|
|
8478
8603
|
|
|
8604
|
+
// clientauth.ts
|
|
8605
|
+
var tokenhashprefix = "sha256:";
|
|
8606
|
+
var defaulttokenlifetimems = 36e5;
|
|
8607
|
+
var defaultpairinglifetimems = 3e5;
|
|
8608
|
+
var defaultchallengelifetimems = 12e4;
|
|
8609
|
+
var authrefusedmessage = "The remote frame failed its authentication handshake.";
|
|
8610
|
+
async function tokenhashof(raw) {
|
|
8611
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(raw));
|
|
8612
|
+
return tokenhashprefix + [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
8613
|
+
}
|
|
8614
|
+
function issuepairingcode(input) {
|
|
8615
|
+
const scopes = input.scopes.filter((scope) => toolnamespaces.includes(scope));
|
|
8616
|
+
return { code: input.code ?? `DT-${randomid().replace(/-/g, "").slice(0, 8).toUpperCase()}`, scopes, issuedat: input.now, expiresat: input.now + (input.lifetime ?? defaultpairinglifetimems) };
|
|
8617
|
+
}
|
|
8618
|
+
function redeempairingcode(input) {
|
|
8619
|
+
const match = input.codes.find((candidate) => candidate.code === input.code);
|
|
8620
|
+
if (match === void 0) return { reason: authrefusedmessage };
|
|
8621
|
+
if (match.usedat !== void 0) return { reason: "The pairing code was already used once and never pairs a second client." };
|
|
8622
|
+
if (input.now >= match.expiresat) return { reason: "The pairing code expired before the exchange completed." };
|
|
8623
|
+
return { code: { ...match, usedat: input.now } };
|
|
8624
|
+
}
|
|
8625
|
+
async function issuetoken(input) {
|
|
8626
|
+
const raw = input.raw ?? `${randomid()}.${randomid()}`;
|
|
8627
|
+
const token = { id: input.id ?? randomid(), clientid: input.clientid, hash: await tokenhashof(raw), scopes: input.scopes.filter((scope) => toolnamespaces.includes(scope)), issuedat: input.now, expiresat: input.now + (input.lifetime ?? defaulttokenlifetimems) };
|
|
8628
|
+
return { token, raw };
|
|
8629
|
+
}
|
|
8630
|
+
async function verifytoken(input) {
|
|
8631
|
+
const hash = await tokenhashof(input.raw);
|
|
8632
|
+
const match = input.tokens.find((candidate) => candidate.hash === hash);
|
|
8633
|
+
if (match === void 0) return { reason: authrefusedmessage };
|
|
8634
|
+
if (match.revokedat !== void 0) return { reason: authrefusedmessage };
|
|
8635
|
+
if (input.now >= match.expiresat) return { reason: authrefusedmessage };
|
|
8636
|
+
return { token: match };
|
|
8637
|
+
}
|
|
8638
|
+
function revokeclient(tokens, clientid, now) {
|
|
8639
|
+
return tokens.map((token) => token.clientid === clientid && token.revokedat === void 0 ? { ...token, revokedat: now } : token);
|
|
8640
|
+
}
|
|
8641
|
+
function checkallowlist(input) {
|
|
8642
|
+
const entry = input.entries.find((candidate) => candidate.fingerprint === input.fingerprint);
|
|
8643
|
+
if (entry === void 0) return { allowed: false, reason: `The client fingerprint ${input.fingerprint} is not on the allowlist and is refused.` };
|
|
8644
|
+
if (input.namespace !== void 0 && !entry.namespaces.includes(input.namespace)) return { allowed: false, reason: `The allowlist entry ${entry.displayname} grants no ${input.namespace} tools.` };
|
|
8645
|
+
return { allowed: true };
|
|
8646
|
+
}
|
|
8647
|
+
function grantallowlistentry(input) {
|
|
8648
|
+
const scopes = input.namespaces.filter((scope) => toolnamespaces.includes(scope));
|
|
8649
|
+
const existing = input.entries.find((entry) => entry.fingerprint === input.identity.fingerprint);
|
|
8650
|
+
if (existing === void 0) {
|
|
8651
|
+
return [{ fingerprint: input.identity.fingerprint, displayname: input.identity.displayname, namespaces: scopes, grantedat: input.now, history: [{ at: input.now, actor: input.actor, change: `Granted the ${scopes.length > 0 ? scopes.join(", ") : "no"} namespaces.` }] }, ...input.entries];
|
|
8652
|
+
}
|
|
8653
|
+
return input.entries.map((entry) => entry.fingerprint !== input.identity.fingerprint ? entry : { ...entry, displayname: input.identity.displayname, namespaces: scopes, history: [{ at: input.now, actor: input.actor, change: `Rescoped to ${scopes.length > 0 ? scopes.join(", ") : "no"} namespaces.` }, ...entry.history] });
|
|
8654
|
+
}
|
|
8655
|
+
function issuechallenge(input) {
|
|
8656
|
+
return { nonce: input.nonce ?? randomid(), method: input.method, issuedat: input.now, expiresat: input.now + (input.lifetime ?? defaultchallengelifetimems) };
|
|
8657
|
+
}
|
|
8658
|
+
function scopecheck(token, namespace) {
|
|
8659
|
+
if (namespace === void 0) return { allowed: false, fast: true, reason: "The tool call names no reviewed namespace." };
|
|
8660
|
+
if (token === void 0) return { allowed: false, reason: "The tool call carries no verified session token." };
|
|
8661
|
+
if (!token.scopes.includes(namespace)) return { allowed: false, reason: `The session token grants no ${namespace} tools.` };
|
|
8662
|
+
return { allowed: true };
|
|
8663
|
+
}
|
|
8664
|
+
function tlsstateof(tls) {
|
|
8665
|
+
return { mode: tls.mode, certificaterequired: tls.mode === "required" || tls.certificatefingerprint !== void 0, verified: tls.verifiedat !== void 0 };
|
|
8666
|
+
}
|
|
8667
|
+
|
|
8668
|
+
// mcpserver.ts
|
|
8669
|
+
var localhostbind = "127.0.0.1";
|
|
8670
|
+
var defaultmcpport = 7436;
|
|
8671
|
+
function rpcerrorof(code, message, data) {
|
|
8672
|
+
return { code, message, ...data !== void 0 ? { data } : {} };
|
|
8673
|
+
}
|
|
8674
|
+
function defaultmcpconfig() {
|
|
8675
|
+
return { port: defaultmcpport, transports: ["stdio", "http"], enabled: false };
|
|
8676
|
+
}
|
|
8677
|
+
function unwraphttppost(value) {
|
|
8678
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
8679
|
+
const candidate = value;
|
|
8680
|
+
if (candidate.transport === "http" && candidate.frame && typeof candidate.frame === "object" && !Array.isArray(candidate.frame)) return candidate.frame;
|
|
8681
|
+
}
|
|
8682
|
+
return value;
|
|
8683
|
+
}
|
|
8684
|
+
function parseframe(raw) {
|
|
8685
|
+
const parsed = unwraphttppost(JSON.parse(raw));
|
|
8686
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("A json rpc frame must be an object.");
|
|
8687
|
+
return parsed;
|
|
8688
|
+
}
|
|
8689
|
+
function serializeframe(frame) {
|
|
8690
|
+
return JSON.stringify(frame);
|
|
8691
|
+
}
|
|
8692
|
+
function validateframe(frame, methods, config) {
|
|
8693
|
+
if (frame.jsonrpc !== "2.0") return rpcerrorof("parse", "The frame must carry the jsonrpc 2.0 tag.");
|
|
8694
|
+
if (frame.id !== void 0 && typeof frame.id !== "number" && typeof frame.id !== "string" && frame.id !== null) return rpcerrorof("parse", "The frame id must be a number, a string or null.");
|
|
8695
|
+
if (frame.method === void 0 || frame.method.trim() === "") return rpcerrorof("method", "The frame carries no method to route.");
|
|
8696
|
+
if (!methods.some((entry) => entry.method === frame.method)) return rpcerrorof("method", `The server routes no method named ${frame.method}.`);
|
|
8697
|
+
if (frame.params !== void 0 && (typeof frame.params !== "object" || Array.isArray(frame.params))) return rpcerrorof("params", "The frame params must be an object.");
|
|
8698
|
+
if (config?.framesize !== void 0 && serializeframe(frame).length > config.framesize) return rpcerrorof("params", `The serialized frame exceeds the user configured frame size of ${config.framesize} characters.`);
|
|
8699
|
+
return void 0;
|
|
8700
|
+
}
|
|
8701
|
+
function respond(input) {
|
|
8702
|
+
return { jsonrpc: "2.0", ...input.id === void 0 ? input.error !== void 0 ? { id: null } : {} : { id: input.id }, ...input.error !== void 0 ? { error: input.error } : { result: input.result } };
|
|
8703
|
+
}
|
|
8704
|
+
function servermethods() {
|
|
8705
|
+
return [
|
|
8706
|
+
{ method: "initialize", handler: "initialize", description: "Completes the mcp handshake and returns the server info." },
|
|
8707
|
+
{ method: "ping", handler: "ping", description: "Answers keepalive frames with pong." },
|
|
8708
|
+
{ method: "tools/list", handler: "listtools", description: "Returns every tool with its version and json schema inputs." },
|
|
8709
|
+
{ method: "negotiate", handler: "negotiate", description: "Exchanges capability sets with the client." },
|
|
8710
|
+
{ method: "tools/call", handler: "dispatch", description: "Invokes one tool behind the consent gates." }
|
|
8711
|
+
];
|
|
8712
|
+
}
|
|
8713
|
+
function servercapabilities(input) {
|
|
8714
|
+
return { protocolversion, name: "devthink", version: protocolversion, toolversion: input.catalog.version, tools: alltools(input.catalog).length, namespaces: toolnamespaces, transports: input.config.transports };
|
|
8715
|
+
}
|
|
8716
|
+
function initialize(input) {
|
|
8717
|
+
void input.params;
|
|
8718
|
+
return { serverinfo: servercapabilities({ config: input.config, catalog: input.catalog }), protocolversion, instructions: "Devthink serves browser tools behind the human review gates: read only tools run once a session is approved while every tool with side effects executes exactly the approved plan step it names. No endpoint, provider or key is hardcoded; the user pairs every client." };
|
|
8719
|
+
}
|
|
8720
|
+
function ping(input) {
|
|
8721
|
+
return { pong: true, at: input.now };
|
|
8722
|
+
}
|
|
8723
|
+
function listtools(catalog) {
|
|
8724
|
+
return { tools: alltools(catalog).map((tool) => ({ name: tool.name, version: tool.version, description: tool.description, inputschema: tool.inputschema, risk: tool.risk, ...tool.consentmeta !== void 0 ? { consentmeta: { review: tool.consentmeta.review, riskclass: tool.consentmeta.riskclass ?? tool.risk, approvalrequired: tool.consentmeta.approvalrequired ?? true, originscope: tool.consentmeta.originscope ?? "session" } } : {} })) };
|
|
8725
|
+
}
|
|
8726
|
+
function negotiate(input) {
|
|
8727
|
+
const client = input.client;
|
|
8728
|
+
if (client?.protocolversion !== void 0 && client.protocolversion !== input.server.protocolversion) return { agreed: false, mismatch: `The client speaks protocol version ${String(client.protocolversion)} while the server offers ${input.server.protocolversion}.` };
|
|
8729
|
+
if (client?.toolversion !== void 0 && client.toolversion > input.server.toolversion) return { agreed: false, mismatch: `The client requires tool version ${String(client.toolversion)} while the server offers ${String(input.server.toolversion)}.` };
|
|
8730
|
+
if (client?.transports !== void 0 && client.transports.some((transport) => !input.server.transports.includes(transport))) return { agreed: false, mismatch: "The client requires a transport the server configuration does not allow." };
|
|
8731
|
+
return { agreed: true, capabilities: input.server };
|
|
8732
|
+
}
|
|
8733
|
+
function connectclient(input) {
|
|
8734
|
+
return { id: input.id ?? `client-${input.now}`, transport: input.transport, paired: false, connectedat: input.now };
|
|
8735
|
+
}
|
|
8736
|
+
function disconnectclient(clients, id, now) {
|
|
8737
|
+
return clients.map((client) => client.id === id && client.disconnectedat === void 0 ? { ...client, disconnectedat: now } : client);
|
|
8738
|
+
}
|
|
8739
|
+
function negotiatetoolfloor(clientfloor, catalogversion) {
|
|
8740
|
+
if (clientfloor === void 0) return { floor: catalogversion };
|
|
8741
|
+
if (clientfloor > catalogversion) return { mismatch: `The client requires the tool version floor ${clientfloor} while the catalog serves version ${catalogversion}.` };
|
|
8742
|
+
return { floor: clientfloor };
|
|
8743
|
+
}
|
|
8744
|
+
async function dispatchtool(input) {
|
|
8745
|
+
const params = input.params;
|
|
8746
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) return { error: rpcerrorof("params", "The tool call needs its params object.") };
|
|
8747
|
+
if (typeof params.name !== "string" || !params.name.trim()) return { error: rpcerrorof("params", "The tool call needs the namespaced name of the tool it invokes.") };
|
|
8748
|
+
const tool = resolvetool(input.catalog, params.name.trim());
|
|
8749
|
+
if (tool === void 0) return { error: rpcerrorof("params", `The catalog holds no unambiguous tool named ${params.name.trim()}.`) };
|
|
8750
|
+
const namespace = namespaceof(tool.name);
|
|
8751
|
+
if (namespace === void 0) return { error: rpcerrorof("params", `The tool ${tool.name} carries no reviewed namespace.`) };
|
|
8752
|
+
if (input.scopes !== void 0 && !input.scopes.includes(namespace)) return { error: rpcerrorof("consentrefused", `The session token grants no ${namespace} tools.`) };
|
|
8753
|
+
const floor = input.client.toolfloor ?? input.client.capabilities?.toolversion ?? input.catalog.version;
|
|
8754
|
+
if (tool.version < floor) return { error: rpcerrorof("params", `The tool ${tool.name} of version ${tool.version} stays below the negotiated compatibility floor of ${floor}.`) };
|
|
8755
|
+
const stepid = typeof params.stepid === "string" ? params.stepid : void 0;
|
|
8756
|
+
const gate = tooldispatchgate({ client: input.client, tool, session: input.session, plan: input.plan, origin: input.origin, ...stepid !== void 0 ? { stepid } : {}, now: input.now });
|
|
8757
|
+
if (!gate.allowed) return { error: rpcerrorof("consentrefused", gate.reason ?? "The consent gates refused the tool call.") };
|
|
8758
|
+
const step = tool.risk === "read" ? { id: `mcp-${input.client.id}-${input.now}`, kind: tool.kind, summary: tool.description.split(".")[0] ?? tool.description, risk: "read", ...typeof params.target === "string" ? { target: params.target } : {}, ...typeof params.value === "string" ? { value: params.value } : {}, ...params.options !== void 0 && typeof params.options === "object" && !Array.isArray(params.options) ? { options: JSON.stringify(params.options) } : {} } : input.plan?.steps.find((candidate) => candidate.id === stepid);
|
|
8759
|
+
if (step === void 0) return { error: rpcerrorof("consentrefused", "The tool call names a step the approved plan does not carry.") };
|
|
8760
|
+
try {
|
|
8761
|
+
const result = await input.execute(step);
|
|
8762
|
+
return { result, step };
|
|
8763
|
+
} catch (error) {
|
|
8764
|
+
return { error: rpcerrorof("internal", error instanceof Error ? error.message : String(error)) };
|
|
8765
|
+
}
|
|
8766
|
+
}
|
|
8767
|
+
async function handleframe(input) {
|
|
8768
|
+
if (input.raw !== void 0 && input.config.framesize !== void 0 && input.raw.length > input.config.framesize) return respond({ id: null, error: rpcerrorof("params", `The wire frame exceeds the user configured frame size of ${input.config.framesize} characters.`) });
|
|
8769
|
+
let frame;
|
|
8770
|
+
if (input.raw !== void 0) {
|
|
8771
|
+
try {
|
|
8772
|
+
frame = parseframe(input.raw);
|
|
8773
|
+
} catch {
|
|
8774
|
+
return respond({ id: null, error: rpcerrorof("parse", "The wire frame does not parse as json.") });
|
|
8775
|
+
}
|
|
8776
|
+
} else if (input.frame !== void 0) {
|
|
8777
|
+
frame = input.frame;
|
|
8778
|
+
} else {
|
|
8779
|
+
return respond({ id: null, error: rpcerrorof("parse", "The server received no frame to route.") });
|
|
8780
|
+
}
|
|
8781
|
+
const invalid = validateframe(frame, servermethods(), input.config);
|
|
8782
|
+
if (invalid !== void 0) return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, error: invalid });
|
|
8783
|
+
const entry = servermethods().find((candidate) => candidate.method === frame.method);
|
|
8784
|
+
if (entry === void 0) return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, error: rpcerrorof("method", `The server routes no method named ${String(frame.method)}.`) });
|
|
8785
|
+
const params = frame.params;
|
|
8786
|
+
if (entry.handler === "initialize") return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, result: initialize({ ...params !== void 0 ? { params } : {}, config: input.config, catalog: input.catalog }) });
|
|
8787
|
+
if (entry.handler === "ping") return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, result: ping({ now: input.now }) });
|
|
8788
|
+
if (entry.handler === "listtools") return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, result: listtools(input.catalog) });
|
|
8789
|
+
if (entry.handler === "negotiate") {
|
|
8790
|
+
const server = servercapabilities({ config: input.config, catalog: input.catalog });
|
|
8791
|
+
const clientcaps = params?.capabilities && typeof params.capabilities === "object" && !Array.isArray(params.capabilities) ? params.capabilities : void 0;
|
|
8792
|
+
const outcome = negotiate({ ...clientcaps !== void 0 ? { client: clientcaps } : {}, server });
|
|
8793
|
+
return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, ...outcome.agreed ? { result: outcome.capabilities } : { error: rpcerrorof("params", outcome.mismatch ?? "The capability negotiation did not agree.") } });
|
|
8794
|
+
}
|
|
8795
|
+
const dispatched = await dispatchtool({ ...params !== void 0 ? { params } : {}, client: input.client, catalog: input.catalog, ...input.session !== void 0 ? { session: input.session } : {}, ...input.plan !== void 0 ? { plan: input.plan } : {}, ...input.scopes !== void 0 ? { scopes: input.scopes } : {}, origin: input.origin, now: input.now, execute: input.execute });
|
|
8796
|
+
return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, ...dispatched.error !== void 0 ? { error: dispatched.error } : { result: dispatched.result } });
|
|
8797
|
+
}
|
|
8798
|
+
function bindlocalhost(config) {
|
|
8799
|
+
const bind = config.bind !== void 0 && config.bind.trim() !== "" ? config.bind.trim() : localhostbind;
|
|
8800
|
+
return { bind, port: config.port, localhost: bind === localhostbind || bind === "localhost" || bind === "::1" };
|
|
8801
|
+
}
|
|
8802
|
+
function launchbridge(input) {
|
|
8803
|
+
return { id: input.id ?? `bridge-${input.now}`, host: input.host, connected: true, ...input.pid !== void 0 ? { pid: input.pid } : {}, startedat: input.now, restarts: 0, received: 0, sent: 0 };
|
|
8804
|
+
}
|
|
8805
|
+
function relayframe(input) {
|
|
8806
|
+
return { ...input.bridge, connected: true, received: input.bridge.received + (input.direction === "inbound" ? 1 : 0), sent: input.bridge.sent + (input.direction === "outbound" ? 1 : 0), lastframeat: input.now };
|
|
8807
|
+
}
|
|
8808
|
+
function toolcallevent(input) {
|
|
8809
|
+
return { id: input.id, clientid: input.clientid, tool: input.tool, origin: input.origin, ok: input.ok, ...input.code !== void 0 ? { code: input.code } : {}, at: input.now };
|
|
8810
|
+
}
|
|
8811
|
+
|
|
8812
|
+
// httpstream.ts
|
|
8813
|
+
var defaultheartbeatms = 3e4;
|
|
8814
|
+
var defaultidlewindowms = 9e4;
|
|
8815
|
+
function defaulthttpstream() {
|
|
8816
|
+
return { endpoint: "/mcp", streampath: "/mcp/stream", tls: { mode: "off" }, heartbeatms: defaultheartbeatms, idlewindowms: defaultidlewindowms };
|
|
8817
|
+
}
|
|
8818
|
+
function openstreamchannel(input) {
|
|
8819
|
+
return { id: input.id ?? `channel-${randomchannelid()}`, clientid: input.clientid, openedat: input.now, lastbeatat: input.now };
|
|
8820
|
+
}
|
|
8821
|
+
function randomchannelid() {
|
|
8822
|
+
return crypto.randomUUID();
|
|
8823
|
+
}
|
|
8824
|
+
function heartbeat(input) {
|
|
8825
|
+
return input.channels.map((channel) => channel.clientid === input.clientid && channel.closedat === void 0 ? { ...channel, lastbeatat: input.now } : channel);
|
|
8826
|
+
}
|
|
8827
|
+
function channellive(channel, now, idlewindow) {
|
|
8828
|
+
if (channel.closedat !== void 0) return false;
|
|
8829
|
+
return now - channel.lastbeatat < (idlewindow ?? defaultidlewindowms);
|
|
8830
|
+
}
|
|
8831
|
+
function closeidlechannels(input) {
|
|
8832
|
+
return input.channels.map((channel) => channel.closedat === void 0 && !channellive(channel, input.now, input.idlewindow) ? { ...channel, closedat: input.now } : channel);
|
|
8833
|
+
}
|
|
8834
|
+
function starttls(input) {
|
|
8835
|
+
if (input.config.mode === "off") return { tls: false, verified: false };
|
|
8836
|
+
if (input.config.mode === "required" && input.presented?.fingerprint === void 0) return { tls: false, verified: false, reason: "The remote transport requires tls and the peer presented no certificate." };
|
|
8837
|
+
if (input.config.certificatefingerprint !== void 0 && input.presented?.fingerprint !== input.config.certificatefingerprint) return { tls: false, verified: false, reason: "The peer certificate does not match the user configured fingerprint and the remote traffic is refused." };
|
|
8838
|
+
return { tls: true, verified: true };
|
|
8839
|
+
}
|
|
8840
|
+
function enforcemaxclients(input) {
|
|
8841
|
+
if (input.maxclients === void 0) return { allowed: true };
|
|
8842
|
+
const connected = input.clients.filter((client) => client.disconnectedat === void 0).length;
|
|
8843
|
+
if (connected >= input.maxclients) return { allowed: false, reason: `The user configured maximum of ${input.maxclients} remote clients is reached and the connection is refused.` };
|
|
8844
|
+
return { allowed: true };
|
|
8845
|
+
}
|
|
8846
|
+
function listremotestatus(input) {
|
|
8847
|
+
const stream = input.config.httpstream ?? defaulthttpstream();
|
|
8848
|
+
const remote = input.config.remoteaccess;
|
|
8849
|
+
const idlewindow = stream.idlewindowms;
|
|
8850
|
+
const open = input.channels.filter((channel) => channellive(channel, input.now, idlewindow));
|
|
8851
|
+
return { endpoint: remote?.endpoint ?? stream.endpoint, tls: tlsstateof(remote?.tls ?? stream.tls), clients: input.clients.filter((client) => client.disconnectedat === void 0).length, paired: input.clients.filter((client) => client.paired && client.disconnectedat === void 0).length, channelsopen: open.length, channelsdead: input.channels.length - open.length, tokenslive: input.tokens.filter((token) => token.revokedat === void 0 && input.now < token.expiresat).length };
|
|
8852
|
+
}
|
|
8853
|
+
async function httpframepipeline(input) {
|
|
8854
|
+
const stream = input.config.httpstream ?? defaulthttpstream();
|
|
8855
|
+
const tls = starttls({ config: input.config.remoteaccess?.tls ?? stream.tls, ...input.presented !== void 0 ? { presented: input.presented } : {}, now: input.now });
|
|
8856
|
+
if (tls.reason !== void 0) return { error: rpcerrorof("consentrefused", tls.reason) };
|
|
8857
|
+
if (input.rawtoken === void 0) return { error: rpcerrorof("consentrefused", authrefusedmessage) };
|
|
8858
|
+
const verified = await verifytoken({ tokens: input.tokens, raw: input.rawtoken, now: input.now });
|
|
8859
|
+
if (verified.token === void 0) return { error: rpcerrorof("consentrefused", verified.reason ?? authrefusedmessage) };
|
|
8860
|
+
const namespace = input.toolname !== void 0 ? namespaceof(input.toolname) : void 0;
|
|
8861
|
+
const listed = checkallowlist({ entries: input.allowlist, fingerprint: input.fingerprint, ...namespace !== void 0 ? { namespace } : {} });
|
|
8862
|
+
if (!listed.allowed) return { error: rpcerrorof("consentrefused", listed.reason ?? "The allowlist refused the client.") };
|
|
8863
|
+
const scoped = scopecheck(verified.token, namespace);
|
|
8864
|
+
if (!scoped.allowed) return { error: rpcerrorof(scoped.fast === true ? "params" : "consentrefused", scoped.reason ?? "The tool call stayed outside the granted scopes.") };
|
|
8865
|
+
return { token: verified.token };
|
|
8866
|
+
}
|
|
8867
|
+
|
|
8868
|
+
// approvalgate.ts
|
|
8869
|
+
var defaultapprovalwindowms = 12e4;
|
|
8870
|
+
function requireapproval(input) {
|
|
8871
|
+
return { id: input.id ?? randomid(), clientid: input.clientid, tool: input.tool, reason: input.reason, params: input.params, ...input.secretfields !== void 0 && input.secretfields.length > 0 ? { secretfields: input.secretfields } : {}, state: "pending", raisedat: input.now, ...input.timeout !== void 0 ? { timeoutat: input.now + input.timeout } : {} };
|
|
8872
|
+
}
|
|
8873
|
+
function resolveapproval(input) {
|
|
8874
|
+
const gate = input.requests.find((request) => request.id === input.id);
|
|
8875
|
+
if (gate === void 0 || gate.state !== "pending") return { requests: input.requests };
|
|
8876
|
+
const decision = input.decision;
|
|
8877
|
+
const requests = input.requests.map((request) => request.id === input.id ? { ...request, state: decision, decidedat: input.now, actor: input.actor } : request);
|
|
8878
|
+
return { requests, exec: { requestid: input.id, decision, actor: input.actor, at: input.now, latencyms: input.now - gate.raisedat } };
|
|
8879
|
+
}
|
|
8880
|
+
function expireapprovals(requests, now) {
|
|
8881
|
+
return requests.map((request) => request.state === "pending" && request.timeoutat !== void 0 && now >= request.timeoutat ? { ...request, state: "expired" } : request);
|
|
8882
|
+
}
|
|
8883
|
+
|
|
8479
8884
|
// protocol.ts
|
|
8480
8885
|
function record(value) {
|
|
8481
8886
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Protocol message must be an object.");
|
|
@@ -10072,142 +10477,6 @@ function yamlscalarvalue(text2) {
|
|
|
10072
10477
|
return text2;
|
|
10073
10478
|
}
|
|
10074
10479
|
|
|
10075
|
-
// mcpserver.ts
|
|
10076
|
-
var localhostbind = "127.0.0.1";
|
|
10077
|
-
var defaultmcpport = 7436;
|
|
10078
|
-
function rpcerrorof(code, message, data) {
|
|
10079
|
-
return { code, message, ...data !== void 0 ? { data } : {} };
|
|
10080
|
-
}
|
|
10081
|
-
function defaultmcpconfig() {
|
|
10082
|
-
return { port: defaultmcpport, transports: ["stdio", "http"], enabled: false };
|
|
10083
|
-
}
|
|
10084
|
-
function unwraphttppost(value) {
|
|
10085
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
10086
|
-
const candidate = value;
|
|
10087
|
-
if (candidate.transport === "http" && candidate.frame && typeof candidate.frame === "object" && !Array.isArray(candidate.frame)) return candidate.frame;
|
|
10088
|
-
}
|
|
10089
|
-
return value;
|
|
10090
|
-
}
|
|
10091
|
-
function parseframe(raw) {
|
|
10092
|
-
const parsed = unwraphttppost(JSON.parse(raw));
|
|
10093
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("A json rpc frame must be an object.");
|
|
10094
|
-
return parsed;
|
|
10095
|
-
}
|
|
10096
|
-
function serializeframe(frame) {
|
|
10097
|
-
return JSON.stringify(frame);
|
|
10098
|
-
}
|
|
10099
|
-
function validateframe(frame, methods, config) {
|
|
10100
|
-
if (frame.jsonrpc !== "2.0") return rpcerrorof("parse", "The frame must carry the jsonrpc 2.0 tag.");
|
|
10101
|
-
if (frame.id !== void 0 && typeof frame.id !== "number" && typeof frame.id !== "string" && frame.id !== null) return rpcerrorof("parse", "The frame id must be a number, a string or null.");
|
|
10102
|
-
if (frame.method === void 0 || frame.method.trim() === "") return rpcerrorof("method", "The frame carries no method to route.");
|
|
10103
|
-
if (!methods.some((entry) => entry.method === frame.method)) return rpcerrorof("method", `The server routes no method named ${frame.method}.`);
|
|
10104
|
-
if (frame.params !== void 0 && (typeof frame.params !== "object" || Array.isArray(frame.params))) return rpcerrorof("params", "The frame params must be an object.");
|
|
10105
|
-
if (config?.framesize !== void 0 && serializeframe(frame).length > config.framesize) return rpcerrorof("params", `The serialized frame exceeds the user configured frame size of ${config.framesize} characters.`);
|
|
10106
|
-
return void 0;
|
|
10107
|
-
}
|
|
10108
|
-
function respond(input) {
|
|
10109
|
-
return { jsonrpc: "2.0", ...input.id === void 0 ? input.error !== void 0 ? { id: null } : {} : { id: input.id }, ...input.error !== void 0 ? { error: input.error } : { result: input.result } };
|
|
10110
|
-
}
|
|
10111
|
-
function servermethods() {
|
|
10112
|
-
return [
|
|
10113
|
-
{ method: "initialize", handler: "initialize", description: "Completes the mcp handshake and returns the server info." },
|
|
10114
|
-
{ method: "ping", handler: "ping", description: "Answers keepalive frames with pong." },
|
|
10115
|
-
{ method: "tools/list", handler: "listtools", description: "Returns every tool with its version and json schema inputs." },
|
|
10116
|
-
{ method: "negotiate", handler: "negotiate", description: "Exchanges capability sets with the client." },
|
|
10117
|
-
{ method: "tools/call", handler: "dispatch", description: "Invokes one tool behind the consent gates." }
|
|
10118
|
-
];
|
|
10119
|
-
}
|
|
10120
|
-
function servercapabilities(input) {
|
|
10121
|
-
return { protocolversion, name: "devthink", version: protocolversion, toolversion: input.catalog.version, tools: alltools(input.catalog).length, namespaces: toolnamespaces, transports: input.config.transports };
|
|
10122
|
-
}
|
|
10123
|
-
function initialize(input) {
|
|
10124
|
-
void input.params;
|
|
10125
|
-
return { serverinfo: servercapabilities({ config: input.config, catalog: input.catalog }), protocolversion, instructions: "Devthink serves browser tools behind the human review gates: read only tools run once a session is approved while every tool with side effects executes exactly the approved plan step it names. No endpoint, provider or key is hardcoded; the user pairs every client." };
|
|
10126
|
-
}
|
|
10127
|
-
function ping(input) {
|
|
10128
|
-
return { pong: true, at: input.now };
|
|
10129
|
-
}
|
|
10130
|
-
function listtools(catalog) {
|
|
10131
|
-
return { tools: alltools(catalog).map((tool) => ({ name: tool.name, version: tool.version, description: tool.description, inputschema: tool.inputschema, risk: tool.risk, ...tool.consentmeta !== void 0 ? { consentmeta: tool.consentmeta.review } : {} })) };
|
|
10132
|
-
}
|
|
10133
|
-
function negotiate(input) {
|
|
10134
|
-
const client = input.client;
|
|
10135
|
-
if (client?.protocolversion !== void 0 && client.protocolversion !== input.server.protocolversion) return { agreed: false, mismatch: `The client speaks protocol version ${String(client.protocolversion)} while the server offers ${input.server.protocolversion}.` };
|
|
10136
|
-
if (client?.toolversion !== void 0 && client.toolversion > input.server.toolversion) return { agreed: false, mismatch: `The client requires tool version ${String(client.toolversion)} while the server offers ${String(input.server.toolversion)}.` };
|
|
10137
|
-
if (client?.transports !== void 0 && client.transports.some((transport) => !input.server.transports.includes(transport))) return { agreed: false, mismatch: "The client requires a transport the server configuration does not allow." };
|
|
10138
|
-
return { agreed: true, capabilities: input.server };
|
|
10139
|
-
}
|
|
10140
|
-
function connectclient(input) {
|
|
10141
|
-
return { id: input.id ?? `client-${input.now}`, transport: input.transport, paired: false, connectedat: input.now };
|
|
10142
|
-
}
|
|
10143
|
-
function disconnectclient(clients, id, now) {
|
|
10144
|
-
return clients.map((client) => client.id === id && client.disconnectedat === void 0 ? { ...client, disconnectedat: now } : client);
|
|
10145
|
-
}
|
|
10146
|
-
async function dispatchtool(input) {
|
|
10147
|
-
const params = input.params;
|
|
10148
|
-
if (!params || typeof params !== "object" || Array.isArray(params)) return { error: rpcerrorof("params", "The tool call needs its params object.") };
|
|
10149
|
-
if (typeof params.name !== "string" || !params.name.trim()) return { error: rpcerrorof("params", "The tool call needs the namespaced name of the tool it invokes.") };
|
|
10150
|
-
const tool = resolvetool(input.catalog, params.name.trim());
|
|
10151
|
-
if (tool === void 0) return { error: rpcerrorof("params", `The catalog holds no unambiguous tool named ${params.name.trim()}.`) };
|
|
10152
|
-
const floor = input.client.capabilities?.toolversion ?? input.catalog.version;
|
|
10153
|
-
if (tool.version < floor) return { error: rpcerrorof("params", `The tool ${tool.name} of version ${tool.version} stays below the negotiated compatibility floor of ${floor}.`) };
|
|
10154
|
-
const stepid = typeof params.stepid === "string" ? params.stepid : void 0;
|
|
10155
|
-
const gate = tooldispatchgate({ client: input.client, tool, session: input.session, plan: input.plan, origin: input.origin, ...stepid !== void 0 ? { stepid } : {}, now: input.now });
|
|
10156
|
-
if (!gate.allowed) return { error: rpcerrorof("consentrefused", gate.reason ?? "The consent gates refused the tool call.") };
|
|
10157
|
-
const step = tool.risk === "read" ? { id: `mcp-${input.client.id}-${input.now}`, kind: tool.kind, summary: tool.description.split(".")[0] ?? tool.description, risk: "read", ...typeof params.target === "string" ? { target: params.target } : {}, ...typeof params.value === "string" ? { value: params.value } : {}, ...params.options !== void 0 && typeof params.options === "object" && !Array.isArray(params.options) ? { options: JSON.stringify(params.options) } : {} } : input.plan?.steps.find((candidate) => candidate.id === stepid);
|
|
10158
|
-
if (step === void 0) return { error: rpcerrorof("consentrefused", "The tool call names a step the approved plan does not carry.") };
|
|
10159
|
-
try {
|
|
10160
|
-
const result = await input.execute(step);
|
|
10161
|
-
return { result, step };
|
|
10162
|
-
} catch (error) {
|
|
10163
|
-
return { error: rpcerrorof("internal", error instanceof Error ? error.message : String(error)) };
|
|
10164
|
-
}
|
|
10165
|
-
}
|
|
10166
|
-
async function handleframe(input) {
|
|
10167
|
-
if (input.raw !== void 0 && input.config.framesize !== void 0 && input.raw.length > input.config.framesize) return respond({ id: null, error: rpcerrorof("params", `The wire frame exceeds the user configured frame size of ${input.config.framesize} characters.`) });
|
|
10168
|
-
let frame;
|
|
10169
|
-
if (input.raw !== void 0) {
|
|
10170
|
-
try {
|
|
10171
|
-
frame = parseframe(input.raw);
|
|
10172
|
-
} catch {
|
|
10173
|
-
return respond({ id: null, error: rpcerrorof("parse", "The wire frame does not parse as json.") });
|
|
10174
|
-
}
|
|
10175
|
-
} else if (input.frame !== void 0) {
|
|
10176
|
-
frame = input.frame;
|
|
10177
|
-
} else {
|
|
10178
|
-
return respond({ id: null, error: rpcerrorof("parse", "The server received no frame to route.") });
|
|
10179
|
-
}
|
|
10180
|
-
const invalid = validateframe(frame, servermethods(), input.config);
|
|
10181
|
-
if (invalid !== void 0) return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, error: invalid });
|
|
10182
|
-
const entry = servermethods().find((candidate) => candidate.method === frame.method);
|
|
10183
|
-
if (entry === void 0) return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, error: rpcerrorof("method", `The server routes no method named ${String(frame.method)}.`) });
|
|
10184
|
-
const params = frame.params;
|
|
10185
|
-
if (entry.handler === "initialize") return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, result: initialize({ ...params !== void 0 ? { params } : {}, config: input.config, catalog: input.catalog }) });
|
|
10186
|
-
if (entry.handler === "ping") return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, result: ping({ now: input.now }) });
|
|
10187
|
-
if (entry.handler === "listtools") return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, result: listtools(input.catalog) });
|
|
10188
|
-
if (entry.handler === "negotiate") {
|
|
10189
|
-
const server = servercapabilities({ config: input.config, catalog: input.catalog });
|
|
10190
|
-
const clientcaps = params?.capabilities && typeof params.capabilities === "object" && !Array.isArray(params.capabilities) ? params.capabilities : void 0;
|
|
10191
|
-
const outcome = negotiate({ ...clientcaps !== void 0 ? { client: clientcaps } : {}, server });
|
|
10192
|
-
return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, ...outcome.agreed ? { result: outcome.capabilities } : { error: rpcerrorof("params", outcome.mismatch ?? "The capability negotiation did not agree.") } });
|
|
10193
|
-
}
|
|
10194
|
-
const dispatched = await dispatchtool({ ...params !== void 0 ? { params } : {}, client: input.client, catalog: input.catalog, ...input.session !== void 0 ? { session: input.session } : {}, ...input.plan !== void 0 ? { plan: input.plan } : {}, origin: input.origin, now: input.now, execute: input.execute });
|
|
10195
|
-
return respond({ ...frame.id !== void 0 ? { id: frame.id } : {}, ...dispatched.error !== void 0 ? { error: dispatched.error } : { result: dispatched.result } });
|
|
10196
|
-
}
|
|
10197
|
-
function bindlocalhost(config) {
|
|
10198
|
-
const bind = config.bind !== void 0 && config.bind.trim() !== "" ? config.bind.trim() : localhostbind;
|
|
10199
|
-
return { bind, port: config.port, localhost: bind === localhostbind || bind === "localhost" || bind === "::1" };
|
|
10200
|
-
}
|
|
10201
|
-
function launchbridge(input) {
|
|
10202
|
-
return { id: input.id ?? `bridge-${input.now}`, host: input.host, connected: true, ...input.pid !== void 0 ? { pid: input.pid } : {}, startedat: input.now, restarts: 0, received: 0, sent: 0 };
|
|
10203
|
-
}
|
|
10204
|
-
function relayframe(input) {
|
|
10205
|
-
return { ...input.bridge, connected: true, received: input.bridge.received + (input.direction === "inbound" ? 1 : 0), sent: input.bridge.sent + (input.direction === "outbound" ? 1 : 0), lastframeat: input.now };
|
|
10206
|
-
}
|
|
10207
|
-
function toolcallevent(input) {
|
|
10208
|
-
return { id: input.id, clientid: input.clientid, tool: input.tool, origin: input.origin, ok: input.ok, ...input.code !== void 0 ? { code: input.code } : {}, at: input.now };
|
|
10209
|
-
}
|
|
10210
|
-
|
|
10211
10480
|
// extension/pagesession.ts
|
|
10212
10481
|
function capturepagestate(sections) {
|
|
10213
10482
|
const wants = (section) => sections.includes(section);
|
|
@@ -18272,7 +18541,154 @@ async function handlerequest(message, sender) {
|
|
|
18272
18541
|
const inputframe = message;
|
|
18273
18542
|
if (typeof inputframe.raw !== "string" || inputframe.raw.trim() === "") throw new Error("The mcp frame intake needs the raw wire frame.");
|
|
18274
18543
|
const transport = inputframe.transport === "http" ? "http" : "stdio";
|
|
18275
|
-
return processmcpframe(inputframe.raw, inputframe.clientid ?? "", transport);
|
|
18544
|
+
return processmcpframe(inputframe.raw, inputframe.clientid ?? "", transport, inputframe.token, inputframe.fingerprint);
|
|
18545
|
+
}
|
|
18546
|
+
case "mcppairing": {
|
|
18547
|
+
const inputpairing = message;
|
|
18548
|
+
const scopes = (Array.isArray(inputpairing.scopes) ? inputpairing.scopes : []).filter((scope) => ["browser", "workflow", "memory", "system"].includes(scope));
|
|
18549
|
+
const code = await issuepairingcodehandler(scopes);
|
|
18550
|
+
return { code: code.code, scopes: code.scopes, issuedat: code.issuedat, expiresat: code.expiresat };
|
|
18551
|
+
}
|
|
18552
|
+
case "mcpchallenge": {
|
|
18553
|
+
const state = await memory.getmcpstate();
|
|
18554
|
+
if (state?.state !== "running") throw new Error("The auth challenge needs the running mcp server.");
|
|
18555
|
+
const now = Date.now();
|
|
18556
|
+
const challenge = issuechallenge({ method: "pairingcode", now });
|
|
18557
|
+
await memory.setmcpstate({ ...state, challenge });
|
|
18558
|
+
await memory.addauthhandshake({ id: randomid(), clientid: "new", method: "pairingcode", outcome: "issued", at: now });
|
|
18559
|
+
await audit("protocol", "The server issued one auth challenge to a new remote client; the handshake verifies the single use nonce before any pairing exchange.", {});
|
|
18560
|
+
return { nonce: challenge.nonce, method: challenge.method, expiresat: challenge.expiresat };
|
|
18561
|
+
}
|
|
18562
|
+
case "mcpexchange": {
|
|
18563
|
+
const inputexchange = message;
|
|
18564
|
+
const state = await memory.getmcpstate();
|
|
18565
|
+
if (state?.state !== "running") throw new Error("The pairing exchange needs the running mcp server.");
|
|
18566
|
+
const now = Date.now();
|
|
18567
|
+
const challenge = state.challenge;
|
|
18568
|
+
const fingerprint = typeof inputexchange.fingerprint === "string" && inputexchange.fingerprint.trim() !== "" ? inputexchange.fingerprint.trim() : "";
|
|
18569
|
+
const refuseexchange = async () => {
|
|
18570
|
+
await memory.addauthhandshake({ id: randomid(), clientid: "new", method: "pairingcode", outcome: "refused", at: now });
|
|
18571
|
+
await audit("protocol", "The pairing exchange failed its auth handshake and was refused; the fixed refusal carries no pairing state.", {});
|
|
18572
|
+
throw new Error("The remote frame failed its authentication handshake.");
|
|
18573
|
+
};
|
|
18574
|
+
if (fingerprint === "" || challenge === void 0 || inputexchange.nonce !== challenge.nonce || now >= challenge.expiresat) await refuseexchange();
|
|
18575
|
+
const redeemed = redeempairingcode({ codes: await memory.getpairingcodes(), code: inputexchange.code ?? "", now });
|
|
18576
|
+
if (redeemed.code === void 0) {
|
|
18577
|
+
await memory.addauthhandshake({ id: randomid(), clientid: "new", method: "pairingcode", outcome: "refused", at: now });
|
|
18578
|
+
await audit("protocol", "The pairing exchange presented a code that does not pair and was refused; a used or expired code never pairs a second client.", {});
|
|
18579
|
+
throw new Error(redeemed.reason ?? "The remote frame failed its authentication handshake.");
|
|
18580
|
+
}
|
|
18581
|
+
await memory.usepairingcode(redeemed.code.code, now);
|
|
18582
|
+
const identity = { fingerprint, displayname: typeof inputexchange.displayname === "string" && inputexchange.displayname.trim() !== "" ? inputexchange.displayname.trim() : `Client ${fingerprint.slice(0, 8)}` };
|
|
18583
|
+
await memory.setclientidentity(identity);
|
|
18584
|
+
await memory.setallowlistentry(grantallowlistentry({ entries: await memory.getallowlist(), identity, namespaces: redeemed.code.scopes, actor: "pairing", now })[0]);
|
|
18585
|
+
const config = await mcpconfigof();
|
|
18586
|
+
const clientid = `client-${now}`;
|
|
18587
|
+
const issued = await issuetoken({ clientid, scopes: redeemed.code.scopes, now, ...config.remoteaccess?.tokenlifetimems !== void 0 ? { lifetime: config.remoteaccess.tokenlifetimems } : { lifetime: defaulttokenlifetimems } });
|
|
18588
|
+
const client = { ...connectclient({ transport: "http", now, id: clientid }), fingerprint, paired: true, pairedat: now };
|
|
18589
|
+
await memory.setclient(client);
|
|
18590
|
+
await memory.setsessiontokens([...await memory.getsessiontokens(), issued.token]);
|
|
18591
|
+
await memory.addauthhandshake({ id: randomid(), clientid, method: "pairingcode", outcome: "verified", at: now });
|
|
18592
|
+
await audit("protocol", `The client ${identity.displayname} (${fingerprint}) exchanged its single use pairing code for a session token of the ${redeemed.code.scopes.join(", ") || "no"} namespace${redeemed.code.scopes.length === 1 ? "" : "s"}; the raw token leaves exactly once and only its digest persists.`, {});
|
|
18593
|
+
return { clientid, token: issued.raw, scopes: issued.token.scopes, expiresat: issued.token.expiresat };
|
|
18594
|
+
}
|
|
18595
|
+
case "mcpremoteconfig": {
|
|
18596
|
+
const inputremote = message;
|
|
18597
|
+
const current = await mcpconfigof();
|
|
18598
|
+
const endpoint = typeof inputremote.endpoint === "string" && inputremote.endpoint.trim() !== "" ? inputremote.endpoint.trim() : current.remoteaccess?.endpoint ?? "https://127.0.0.1:7436";
|
|
18599
|
+
const tlsmode = inputremote.tlsmode === "off" || inputremote.tlsmode === "on" || inputremote.tlsmode === "required" ? inputremote.tlsmode : current.remoteaccess?.tls.mode ?? "off";
|
|
18600
|
+
const certificatefingerprint = typeof inputremote.certificatefingerprint === "string" && inputremote.certificatefingerprint.trim() !== "" ? inputremote.certificatefingerprint.trim() : current.remoteaccess?.tls.certificatefingerprint;
|
|
18601
|
+
const config = {
|
|
18602
|
+
...current,
|
|
18603
|
+
...inputremote.reviewed === true || current.remote === true ? { remote: true } : {},
|
|
18604
|
+
httpstream: {
|
|
18605
|
+
endpoint: current.httpstream?.endpoint ?? "/mcp",
|
|
18606
|
+
streampath: typeof inputremote.streampath === "string" && inputremote.streampath.trim() !== "" ? inputremote.streampath.trim() : current.httpstream?.streampath ?? "/mcp/stream",
|
|
18607
|
+
tls: { mode: tlsmode, ...certificatefingerprint !== void 0 ? { certificatefingerprint } : {}, ...current.httpstream?.tls.verifiedat !== void 0 ? { verifiedat: current.httpstream.tls.verifiedat } : {} },
|
|
18608
|
+
...inputremote.heartbeatms !== void 0 ? { heartbeatms: inputremote.heartbeatms } : current.httpstream?.heartbeatms !== void 0 ? { heartbeatms: current.httpstream.heartbeatms } : {},
|
|
18609
|
+
...inputremote.idlewindowms !== void 0 ? { idlewindowms: inputremote.idlewindowms } : current.httpstream?.idlewindowms !== void 0 ? { idlewindowms: current.httpstream.idlewindowms } : {}
|
|
18610
|
+
},
|
|
18611
|
+
remoteaccess: {
|
|
18612
|
+
endpoint,
|
|
18613
|
+
tls: { mode: tlsmode, ...certificatefingerprint !== void 0 ? { certificatefingerprint } : {} },
|
|
18614
|
+
...inputremote.maxclients !== void 0 ? { maxclients: inputremote.maxclients } : current.remoteaccess?.maxclients !== void 0 ? { maxclients: current.remoteaccess.maxclients } : {},
|
|
18615
|
+
...inputremote.tokenlifetime !== void 0 ? { tokenlifetimems: inputremote.tokenlifetime } : current.remoteaccess?.tokenlifetimems !== void 0 ? { tokenlifetimems: current.remoteaccess.tokenlifetimems } : {},
|
|
18616
|
+
...inputremote.approvaltimeoutms !== void 0 ? { approvaltimeout: { windowms: inputremote.approvaltimeoutms, ontimeout: "refuse" } } : current.remoteaccess?.approvaltimeout !== void 0 ? { approvaltimeout: current.remoteaccess.approvaltimeout } : {}
|
|
18617
|
+
}
|
|
18618
|
+
};
|
|
18619
|
+
const gate = remoteenablementgate(config);
|
|
18620
|
+
if (!gate.allowed) throw new Error(gate.reason ?? "The remote transport config failed its gate.");
|
|
18621
|
+
const enablement = serverenablementgate(config);
|
|
18622
|
+
if (!enablement.allowed) throw new Error(enablement.reason ?? "The remote transport config failed the enablement gate.");
|
|
18623
|
+
await memory.setmcpconfig(config);
|
|
18624
|
+
await audit("protocol", `The user configured the remote transport for the endpoint ${endpoint} with the ${tlsmode} tls mode${certificatefingerprint !== void 0 ? " and the reviewed certificate fingerprint" : ""}${config.remoteaccess?.maxclients !== void 0 ? `, a client ceiling of ${config.remoteaccess.maxclients}` : " and no client ceiling"}${config.remoteaccess?.tokenlifetimems !== void 0 ? `, a token lifetime of ${config.remoteaccess.tokenlifetimems} milliseconds` : ""}${config.remoteaccess?.approvaltimeout !== void 0 ? ` and an approval window of ${config.remoteaccess.approvaltimeout.windowms} milliseconds` : ""}; every value stays the user choice.`, {});
|
|
18625
|
+
return mcpstateof();
|
|
18626
|
+
}
|
|
18627
|
+
case "mcpallowlist": {
|
|
18628
|
+
const inputallow = message;
|
|
18629
|
+
const fingerprint = typeof inputallow.fingerprint === "string" && inputallow.fingerprint.trim() !== "" ? inputallow.fingerprint.trim() : "";
|
|
18630
|
+
if (fingerprint === "") throw new Error("The allowlist edit needs the client fingerprint.");
|
|
18631
|
+
const now = Date.now();
|
|
18632
|
+
if (inputallow.remove === true) {
|
|
18633
|
+
const entries2 = await memory.getallowlist();
|
|
18634
|
+
const removed = entries2.find((entry) => entry.fingerprint === fingerprint);
|
|
18635
|
+
await memory.removeallowlistentry(fingerprint);
|
|
18636
|
+
await audit("protocol", `The user refused the client ${removed?.displayname ?? fingerprint} its allowlist entry; its fingerprint stops passing the allowlist check.`, {});
|
|
18637
|
+
return mcpstateof();
|
|
18638
|
+
}
|
|
18639
|
+
const namespaces = (Array.isArray(inputallow.namespaces) ? inputallow.namespaces : []).filter((scope) => ["browser", "workflow", "memory", "system"].includes(scope));
|
|
18640
|
+
const identities = await memory.getclientidentities();
|
|
18641
|
+
const known = identities.find((identity2) => identity2.fingerprint === fingerprint);
|
|
18642
|
+
const identity = { fingerprint, displayname: typeof inputallow.displayname === "string" && inputallow.displayname.trim() !== "" ? inputallow.displayname.trim() : known?.displayname ?? `Client ${fingerprint.slice(0, 8)}` };
|
|
18643
|
+
await memory.setclientidentity(identity);
|
|
18644
|
+
const entries = grantallowlistentry({ entries: await memory.getallowlist(), identity, namespaces, actor: "user", now });
|
|
18645
|
+
const granted = entries[0];
|
|
18646
|
+
if (granted === void 0) throw new Error("The allowlist grant failed.");
|
|
18647
|
+
const valid = allowlistentryvalid(granted, await memory.getclientidentities());
|
|
18648
|
+
if (!valid.allowed) throw new Error(valid.reason ?? "The allowlist entry failed its validation.");
|
|
18649
|
+
await memory.setallowlistentry(granted);
|
|
18650
|
+
await audit("protocol", `The user allowed the client ${identity.displayname} (${fingerprint}) the ${namespaces.join(", ") || "no"} namespace${namespaces.length === 1 ? "" : "s"}; the grant history rides the record.`, {});
|
|
18651
|
+
return mcpstateof();
|
|
18652
|
+
}
|
|
18653
|
+
case "mcprevokeclient": {
|
|
18654
|
+
const inputrevoke = message;
|
|
18655
|
+
const clientid = inputrevoke.clientid ?? "";
|
|
18656
|
+
if (clientid === "") throw new Error("The revocation needs the client id.");
|
|
18657
|
+
const gate = revocationgate();
|
|
18658
|
+
if (!gate.allowed) throw new Error(gate.reason ?? "The revocation failed.");
|
|
18659
|
+
const now = Date.now();
|
|
18660
|
+
const tokens = revokeclient(await memory.getsessiontokens(), clientid, now);
|
|
18661
|
+
await memory.setsessiontokens(tokens);
|
|
18662
|
+
const client = (await memory.getclients()).find((entry) => entry.id === clientid);
|
|
18663
|
+
if (client !== void 0 && client.disconnectedat === void 0) await memory.setclient(disconnectclient(await memory.getclients(), clientid, now).find((entry) => entry.id === clientid));
|
|
18664
|
+
const revoked = tokens.filter((token) => token.clientid === clientid && token.revokedat === now).length;
|
|
18665
|
+
await audit("protocol", `The user revoked the paired client ${clientid}; ${revoked} session token${revoked === 1 ? "" : "s"} stopped verifying and every further frame of the client refuses \u2014 the revocation stays available at any time.`, {});
|
|
18666
|
+
return mcpstateof();
|
|
18667
|
+
}
|
|
18668
|
+
case "mcpapprovaldecision": {
|
|
18669
|
+
const inputapproval = message;
|
|
18670
|
+
const approvalid = inputapproval.approvalid ?? "";
|
|
18671
|
+
if (approvalid === "" || typeof inputapproval.approved !== "boolean") throw new Error("The approval decision needs the gate id and the reviewed approved flag.");
|
|
18672
|
+
const now = Date.now();
|
|
18673
|
+
const stored = await memory.listapprovals();
|
|
18674
|
+
const outcome = resolveapproval({ requests: stored, id: approvalid, decision: inputapproval.approved ? "approved" : "refused", actor: "user", now });
|
|
18675
|
+
if (outcome.exec === void 0) throw new Error(`No pending approval gate matches ${approvalid}.`);
|
|
18676
|
+
for (const request of outcome.requests) await memory.setapproval(request);
|
|
18677
|
+
await memory.addapprovalexec(outcome.exec);
|
|
18678
|
+
const gate = stored.find((request) => request.id === approvalid);
|
|
18679
|
+
if (gate !== void 0 && inputapproval.approved) {
|
|
18680
|
+
const tool = resolvetool(buildtoolcatalog(), gate.tool);
|
|
18681
|
+
const stepid = typeof gate.params.stepid === "string" ? gate.params.stepid : void 0;
|
|
18682
|
+
const plan = await memory.getplan();
|
|
18683
|
+
const step = tool !== void 0 && tool.risk !== "read" && plan !== void 0 && stepid !== void 0 ? plan.steps.find((candidate) => candidate.id === stepid) : void 0;
|
|
18684
|
+
if (step === void 0) throw new Error("The approved gate names no step the approved plan carries; the call refuses.");
|
|
18685
|
+
const result = await executemcpstep(step);
|
|
18686
|
+
await memory.addtoolcall(toolcallevent({ id: randomid(), clientid: gate.clientid, tool: gate.tool, origin: (await memory.getsession())?.origin ?? "", ok: !result.iserror, now }));
|
|
18687
|
+
await audit("tool", `The approval gate ${approvalid} executed the ${gate.tool} call of the client ${gate.clientid} after the user approved it in ${outcome.exec.latencyms} milliseconds; no payload rides the record.`, {});
|
|
18688
|
+
} else {
|
|
18689
|
+
await audit("protocol", `The user refused the approval gate ${approvalid} for the ${gate?.tool ?? "tool"} call of the client ${gate?.clientid ?? "unknown"}; the pending call never executes.`, {});
|
|
18690
|
+
}
|
|
18691
|
+
return mcpstateof();
|
|
18276
18692
|
}
|
|
18277
18693
|
case "runtobreakpoint": {
|
|
18278
18694
|
const inputdebug = message;
|
|
@@ -18644,7 +19060,48 @@ async function mcpstateof() {
|
|
|
18644
19060
|
const config = await mcpconfigof();
|
|
18645
19061
|
const state = await memory.getmcpstate();
|
|
18646
19062
|
const binding = bindlocalhost(config);
|
|
18647
|
-
|
|
19063
|
+
const now = Date.now();
|
|
19064
|
+
await mcpmaintenance(now);
|
|
19065
|
+
const tokens = await memory.getsessiontokens();
|
|
19066
|
+
const clients = await memory.listclients();
|
|
19067
|
+
const channels = closeidlechannels({ channels: await memory.getstreamchannels(), now, ...config.httpstream?.idlewindowms !== void 0 ? { idlewindow: config.httpstream.idlewindowms } : {} });
|
|
19068
|
+
await memory.setstreamchannels(channels);
|
|
19069
|
+
const streamstatus = listremotestatus;
|
|
19070
|
+
return { state: state?.state ?? "stopped", config, bind: binding.bind, port: binding.port, localhost: binding.localhost, clients, ...state?.bridge !== void 0 ? { bridge: state.bridge } : {}, calls: (await memory.listtoolcalls()).slice(0, 25), catalog: listtools(buildtoolcatalog()), launches: (await memory.listbridgelaunches()).slice(0, 10), remote: streamstatus({ config, channels, clients, tokens, now }), pairing: (await memory.getpairingcodes()).filter((code) => code.usedat === void 0 && now < code.expiresat), allowlist: await memory.getallowlist(), tokens, identities: await memory.getclientidentities(), handshakes: (await memory.listauthhandshakes()).slice(0, 10), channels, approvals: await memory.listapprovals() };
|
|
19071
|
+
}
|
|
19072
|
+
async function mcpmaintenance(now) {
|
|
19073
|
+
const tokens = await memory.getsessiontokens();
|
|
19074
|
+
const expired = tokens.filter((token) => token.revokedat === void 0 && now >= token.expiresat);
|
|
19075
|
+
if (expired.length > 0) {
|
|
19076
|
+
await memory.setsessiontokens(tokens.map((token) => expired.includes(token) ? { ...token, revokedat: now } : token));
|
|
19077
|
+
await audit("protocol", `${expired.length} session token${expired.length === 1 ? "" : "s"} reached the user configured lifetime and the server refused ${expired.length === 1 ? "it" : "them"}; the records stay for the audit trail.`, {});
|
|
19078
|
+
}
|
|
19079
|
+
const stored = await memory.listapprovals();
|
|
19080
|
+
const approvals = expireapprovals(stored, now);
|
|
19081
|
+
for (let index = 0; index < approvals.length; index += 1) {
|
|
19082
|
+
const request = approvals[index];
|
|
19083
|
+
if (request !== void 0 && request.state === "expired" && stored[index]?.state === "pending") {
|
|
19084
|
+
await memory.setapproval(request);
|
|
19085
|
+
await audit("protocol", `The approval gate ${request.id} for the ${request.tool} call of the client ${request.clientid} expired unanswered and refused by default; the call never executes.`, {});
|
|
19086
|
+
}
|
|
19087
|
+
}
|
|
19088
|
+
}
|
|
19089
|
+
async function issuepairingcodehandler(scopes) {
|
|
19090
|
+
const session = await memory.getsession();
|
|
19091
|
+
const gate = pairingreadinessgate(session, Date.now());
|
|
19092
|
+
if (!gate.allowed) throw new Error(gate.reason ?? "The pairing flow is refused.");
|
|
19093
|
+
const code = issuepairingcode({ now: Date.now(), scopes });
|
|
19094
|
+
await memory.addpairingcode(code);
|
|
19095
|
+
await audit("protocol", `The user issued the pairing code for the ${code.scopes.join(", ") || "no"} namespace${code.scopes.length === 1 ? "" : "s"}; the code pairs one client once and expires in ${Math.round((code.expiresat - code.issuedat) / 1e3)} seconds.`, {});
|
|
19096
|
+
return code;
|
|
19097
|
+
}
|
|
19098
|
+
async function raiseremoteapproval(clientid, toolname, params, step) {
|
|
19099
|
+
const config = await mcpconfigof();
|
|
19100
|
+
const tool = resolvetool(buildtoolcatalog(), toolname);
|
|
19101
|
+
const request = requireapproval({ clientid, tool: toolname, reason: tool?.consentmeta?.review ?? `The ${toolname} tool has side effects and needs the approval gate.`, params: { ...params, stepid: step.id }, now: Date.now(), ...config.remoteaccess?.approvaltimeout !== void 0 ? { timeout: config.remoteaccess.approvaltimeout.windowms } : { timeout: defaultapprovalwindowms } });
|
|
19102
|
+
await memory.setapproval(request);
|
|
19103
|
+
await audit("protocol", `Raised the approval gate ${request.id} for the ${toolname} call of the remote client ${clientid}; the gate refuses by default after its window and no payload rides the audit.`, {});
|
|
19104
|
+
return { content: `The approval gate ${request.id} holds the ${toolname} call; it executes once the user approves it in the panel.`, payload: { approvalid: request.id, state: "pending", ...request.timeoutat !== void 0 ? { timeoutat: request.timeoutat } : {} }, iserror: false };
|
|
18648
19105
|
}
|
|
18649
19106
|
async function executelistruns(step, session) {
|
|
18650
19107
|
const options = stepoptions2(step);
|
|
@@ -18691,11 +19148,14 @@ async function trybridgelaunch(restart) {
|
|
|
18691
19148
|
function restartbridgeof(bridge) {
|
|
18692
19149
|
return { ...bridge, connected: true, restarts: bridge.restarts + 1, startedat: Date.now() };
|
|
18693
19150
|
}
|
|
18694
|
-
async function routemcpframe(client, frame, config) {
|
|
19151
|
+
async function routemcpframe(client, frame, config, scopes) {
|
|
18695
19152
|
const session = await memory.getsession();
|
|
18696
19153
|
const plan = await memory.getplan();
|
|
18697
19154
|
const catalog = buildtoolcatalog();
|
|
18698
|
-
const
|
|
19155
|
+
const calledtool = frame.method === "tools/call" && typeof frame.params?.name === "string" ? resolvetool(catalog, frame.params.name.trim()) : void 0;
|
|
19156
|
+
const gatedname = client.transport === "http" && calledtool !== void 0 && calledtool.risk !== "read" ? calledtool.name : void 0;
|
|
19157
|
+
const callparams = frame.params ?? {};
|
|
19158
|
+
const response = await handleframe({ frame, client, catalog, config, ...session !== void 0 ? { session } : {}, ...plan !== void 0 ? { plan } : {}, ...scopes !== void 0 ? { scopes } : {}, origin: session?.origin ?? "", tabid: session?.tabid ?? 0, now: Date.now(), execute: gatedname !== void 0 ? (step) => raiseremoteapproval(client.id, gatedname, callparams, step) : executemcpstep });
|
|
18699
19159
|
const now = Date.now();
|
|
18700
19160
|
if (frame.method === "initialize") {
|
|
18701
19161
|
const clientinfo = frame.params?.clientinfo && typeof frame.params.clientinfo === "object" && !Array.isArray(frame.params.clientinfo) ? frame.params.clientinfo : void 0;
|
|
@@ -18709,7 +19169,16 @@ async function routemcpframe(client, frame, config) {
|
|
|
18709
19169
|
if (frame.method === "tools/list") await audit("protocol", `The mcp client ${client.id} listed the tool catalog of ${listtools(catalog).tools.length} tools with their json schema inputs; the listing carries no page data.`, {});
|
|
18710
19170
|
if (frame.method === "negotiate") {
|
|
18711
19171
|
const agreed = response.error === void 0;
|
|
18712
|
-
if (agreed && response.result !== void 0)
|
|
19172
|
+
if (agreed && response.result !== void 0) {
|
|
19173
|
+
await memory.setclientcapabilities(client.id, response.result);
|
|
19174
|
+
const clientcaps = frame.params?.capabilities && typeof frame.params.capabilities === "object" && !Array.isArray(frame.params.capabilities) ? frame.params.capabilities : void 0;
|
|
19175
|
+
const clientversion = typeof clientcaps?.toolversion === "number" && Number.isFinite(clientcaps.toolversion) ? clientcaps.toolversion : void 0;
|
|
19176
|
+
const floor = negotiatetoolfloor(clientversion, catalog.version);
|
|
19177
|
+
if ("floor" in floor) {
|
|
19178
|
+
const stored = (await memory.getclients()).find((entry) => entry.id === client.id);
|
|
19179
|
+
if (stored !== void 0) await memory.setclient({ ...stored, toolfloor: floor.floor });
|
|
19180
|
+
}
|
|
19181
|
+
}
|
|
18713
19182
|
await audit("protocol", `The mcp client ${client.id} ${agreed ? "negotiated its capability set with the server and the negotiated floor stays stored on its record" : `failed the capability negotiation: ${response.error?.message ?? "the sets did not agree"}`}.`, {});
|
|
18714
19183
|
}
|
|
18715
19184
|
if (frame.method === "tools/call") {
|
|
@@ -18721,11 +19190,11 @@ async function routemcpframe(client, frame, config) {
|
|
|
18721
19190
|
const stepid = typeof frame.params?.stepid === "string" ? frame.params.stepid : "mcp";
|
|
18722
19191
|
await memory.setprogress(recordtoolcall(await memory.getprogress(), plan.id, stepid, { clientid: client.id, tool: name, ok, ...code !== void 0 ? { code } : {} }, now));
|
|
18723
19192
|
}
|
|
18724
|
-
await audit("tool", `The mcp client ${client.id} called the ${name} tool on ${session?.origin ?? "no origin"} and ${ok ? "it ran behind the consent gates" : `it was refused${code !== void 0 ? ` with the ${code} error` : ""}`}; no payload rides the record.`, {});
|
|
19193
|
+
await audit("tool", `The mcp client ${client.id} called the ${name} tool on ${session?.origin ?? "no origin"} and ${ok ? gatedname !== void 0 ? "its approval gate waits for the user decision" : "it ran behind the consent gates" : `it was refused${code !== void 0 ? ` with the ${code} error` : ""}`}; no payload rides the record.`, {});
|
|
18725
19194
|
}
|
|
18726
19195
|
return response;
|
|
18727
19196
|
}
|
|
18728
|
-
async function processmcpframe(raw, clientid, transport) {
|
|
19197
|
+
async function processmcpframe(raw, clientid, transport, rawtoken, fingerprint) {
|
|
18729
19198
|
const state = await memory.getmcpstate();
|
|
18730
19199
|
if (state?.state !== "running") return { jsonrpc: "2.0", id: null, error: rpcerrorof("consentrefused", "The mcp server is not running and no frame is routed.") };
|
|
18731
19200
|
const config = await mcpconfigof();
|
|
@@ -18737,14 +19206,37 @@ async function processmcpframe(raw, clientid, transport) {
|
|
|
18737
19206
|
await audit("protocol", "The mcp server refused a wire frame that does not parse as json; the parse error answered the client.", {});
|
|
18738
19207
|
return { jsonrpc: "2.0", id: null, error: rpcerrorof("parse", "The wire frame does not parse as json.") };
|
|
18739
19208
|
}
|
|
18740
|
-
let
|
|
19209
|
+
let scopes;
|
|
19210
|
+
let routedclient = clientid;
|
|
19211
|
+
if (transport === "http") {
|
|
19212
|
+
const now = Date.now();
|
|
19213
|
+
const ceiling = enforcemaxclients({ clients: await memory.getclients(), ...config.remoteaccess?.maxclients !== void 0 ? { maxclients: config.remoteaccess.maxclients } : {} });
|
|
19214
|
+
if (!ceiling.allowed) {
|
|
19215
|
+
await audit("protocol", `The remote connection of the client ${clientid || "unknown"} was refused because the user configured client ceiling is reached.`, {});
|
|
19216
|
+
return { jsonrpc: "2.0", id: frame.id ?? null, error: rpcerrorof("consentrefused", ceiling.reason ?? "The remote connection was refused.") };
|
|
19217
|
+
}
|
|
19218
|
+
const toolname = frame.method === "tools/call" && typeof frame.params?.name === "string" ? frame.params.name : void 0;
|
|
19219
|
+
const pipeline = await httpframepipeline({ config, ...fingerprint !== void 0 ? { presented: { fingerprint } } : {}, tokens: await memory.getsessiontokens(), ...rawtoken !== void 0 ? { rawtoken } : {}, allowlist: await memory.getallowlist(), fingerprint: fingerprint ?? "", ...toolname !== void 0 ? { toolname } : {}, now });
|
|
19220
|
+
if (pipeline.error !== void 0) {
|
|
19221
|
+
await memory.addauthhandshake({ id: randomid(), clientid: clientid || "unknown", method: "token", outcome: "refused", at: now });
|
|
19222
|
+
await audit("protocol", `The remote frame of the client ${clientid || "unknown"} failed the ordered intake pipeline and was refused with the ${pipeline.error.code} error; no pairing state rides the record.`, {});
|
|
19223
|
+
return { jsonrpc: "2.0", id: frame.id ?? null, error: pipeline.error };
|
|
19224
|
+
}
|
|
19225
|
+
scopes = pipeline.token?.scopes;
|
|
19226
|
+
routedclient = pipeline.token?.clientid ?? clientid;
|
|
19227
|
+
const channels = await memory.getstreamchannels();
|
|
19228
|
+
const open = channels.filter((channel) => channel.clientid === routedclient && channel.closedat === void 0);
|
|
19229
|
+
await memory.setstreamchannels(open.length > 0 ? heartbeat({ channels, clientid: routedclient, now }) : [...channels, openstreamchannel({ clientid: routedclient, now })]);
|
|
19230
|
+
}
|
|
19231
|
+
let client = (await memory.getclients()).find((entry) => entry.id === routedclient && entry.disconnectedat === void 0);
|
|
18741
19232
|
if (client === void 0) {
|
|
19233
|
+
if (transport === "http") return { jsonrpc: "2.0", id: frame.id ?? null, error: rpcerrorof("consentrefused", "The remote frame names no paired client; pair the client through the pairing exchange first.") };
|
|
18742
19234
|
client = connectclient({ transport, now: Date.now(), ...clientid !== "" ? { id: clientid } : {} });
|
|
18743
19235
|
await memory.setclient(client);
|
|
18744
19236
|
await audit("protocol", `A new mcp client ${client.id} connected on the ${transport} transport and waits for the pairing approval; unpaired clients never dispatch tools.`, {});
|
|
18745
19237
|
}
|
|
18746
19238
|
const previous = mcpclientchains.get(client.id) ?? Promise.resolve();
|
|
18747
|
-
const task = previous.catch(() => void 0).then(async () => await routemcpframe(client, frame, config));
|
|
19239
|
+
const task = previous.catch(() => void 0).then(async () => await routemcpframe(client, frame, config, scopes));
|
|
18748
19240
|
mcpclientchains.set(client.id, task);
|
|
18749
19241
|
return task;
|
|
18750
19242
|
}
|