// src/tool-pack.ts
function isDynamicPack(pack) {
return pack.kind === "dynamic";
}
function ok(data) {
return {
content: [{ type: "text", text: JSON.stringify(data) }],
structuredContent: data
};
}
function customResult(result) {
return result;
}
function errorResult(message) {
return {
content: [{ type: "text", text: message }],
isError: true
};
}
// src/bridge/registry.ts
function parsePackList(raw) {
if (raw === void 0) return null;
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
}
var DEFAULT_MCP_URL = "/mcp";
function normalizeMcpUrl(raw) {
const v = (raw ?? "").trim();
if (v.toLowerCase() === "none") return void 0;
if (v === "") {
console.warn(
`[webmcp-interceptor] No data-mcp-url configured; defaulting to /mcp. This probes a same-origin endpoint with the user's session credentials. Set data-mcp-url="none" to opt out, or specify the endpoint explicitly.`
);
return DEFAULT_MCP_URL;
}
return v;
}
function selectActivePacks(all, requested, defaults) {
const names = requested ?? defaults;
const byName = new Map(all.map((p) => [p.name, p]));
const active = [];
for (const name of names) {
const pack = byName.get(name);
if (pack) active.push(pack);
else
console.warn(
`[webmcp-interceptor] Requested pack "${name}" is not bundled; skipping.`
);
}
return active;
}
function resolveStaticPacks(packs) {
const resolved = [];
for (const pack of packs) {
if (!isDynamicPack(pack)) {
resolved.push({
name: pack.name,
tools: pack.tools,
handlers: pack.handlers,
dynamic: false
});
}
}
return resolved;
}
async function resolveDynamicPacks(packs, ctx) {
const dynamics = packs.filter(isDynamicPack);
return Promise.all(
dynamics.map(async (pack) => {
try {
const { tools, handlers } = await pack.resolve(ctx);
return { name: pack.name, tools, handlers, dynamic: true };
} catch (err) {
console.warn(
`[webmcp-interceptor] Dynamic pack "${pack.name}" failed to resolve; skipping.`,
err
);
return { name: pack.name, tools: [], handlers: {}, dynamic: true };
}
})
);
}
var MAX_TOTAL_TOOLS = 200;
function buildRegistry(packs) {
const tools = [];
const handlers = {};
const seen = /* @__PURE__ */ new Map();
for (const pack of packs) {
const toolNames = new Set(pack.tools.map((t) => t.name));
for (const tool of pack.tools) {
if (!pack.handlers[tool.name]) {
throw new Error(
`Tool-pack "${pack.name}" declares tool "${tool.name}" but has no matching handler.`
);
}
}
for (const name of Object.keys(pack.handlers)) {
if (!toolNames.has(name)) {
throw new Error(
`Tool-pack "${pack.name}" has a handler for "${name}" with no matching tool descriptor.`
);
}
}
for (const tool of pack.tools) {
const existing = seen.get(tool.name);
if (existing) {
if (pack.dynamic) {
console.warn(
`[webmcp-interceptor] Tool "${tool.name}" from dynamic pack "${pack.name}" collides with "${existing}"; skipping the dynamic one.`
);
continue;
}
throw new Error(
`Tool name collision: "${tool.name}" registered by both "${existing}" and "${pack.name}".`
);
}
if (tools.length >= MAX_TOTAL_TOOLS) {
console.warn(
`[webmcp-interceptor] Reached ${MAX_TOTAL_TOOLS}-tool cap; skipping remaining tools from "${pack.name}".`
);
return { tools, handlers };
}
const handler = pack.handlers[tool.name];
if (!handler) {
throw new Error(
`Tool-pack "${pack.name}" declares tool "${tool.name}" but has no matching handler.`
);
}
seen.set(tool.name, pack.name);
tools.push(tool);
handlers[tool.name] = handler;
}
}
return { tools, handlers };
}
// src/bridge/packs/c2pa/cbor.ts
var BREAK = 255;
var textDecoder = new TextDecoder("utf-8", { fatal: false });
var MAX_ITEMS = 1e6;
var MAX_INDEFINITE_BYTES = 16 * 1024 * 1024;
function decodeCbor(bytes) {
return new CborDecoder(bytes).decode();
}
function isCborTag(v) {
return typeof v === "object" && v !== null && !(v instanceof Uint8Array) && !Array.isArray(v) && !(v instanceof Map) && "tag" in v;
}
function isCborSimple(v) {
return typeof v === "object" && v !== null && !(v instanceof Uint8Array) && !Array.isArray(v) && !(v instanceof Map) && "simple" in v;
}
var CborDecoder = class {
constructor(bytes) {
this.bytes = bytes;
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
}
bytes;
view;
off = 0;
itemCount = 0;
decode() {
return this.readValue(0);
}
/** Read one item. `depth` guards against pathological nesting. */
readValue(depth) {
if (depth > 256) throw new Error("CBOR nesting too deep");
if (++this.itemCount > MAX_ITEMS) throw new Error("CBOR item count exceeds budget");
const ib = this.u8();
const major = ib >> 5;
const ai = ib & 31;
switch (major) {
case 0:
return this.readUint(ai);
case 1: {
const n = this.readUint(ai);
return typeof n === "bigint" ? -1n - n : -1 - n;
}
case 2:
return this.readByteString(ai, depth, 2);
case 3:
return textDecoder.decode(this.readByteString(ai, depth, 3));
case 4:
return this.readArray(ai, depth);
case 5:
return this.readMap(ai, depth);
case 6: {
const tag = this.asLength(this.readUint(ai));
return { tag, value: this.readValue(depth + 1) };
}
default:
return this.readSimple(ai);
}
}
/** Read the argument that follows the initial byte (major 0/1/6). */
readUint(ai) {
if (ai < 24) return ai;
if (ai === 24) return this.u8();
if (ai === 25) {
const v = this.view.getUint16(this.off);
this.off += 2;
return v;
}
if (ai === 26) {
const v = this.view.getUint32(this.off);
this.off += 4;
return v;
}
if (ai === 27) {
const v = this.view.getBigUint64(this.off);
this.off += 8;
return v <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(v) : v;
}
throw new Error(`Invalid CBOR additional-info value: ${ai}`);
}
readByteString(ai, depth, majorType) {
if (ai === 31) {
const chunks = [];
let total = 0;
for (; ; ) {
const ib = this.peek();
if (ib === BREAK) {
this.off += 1;
break;
}
const major = ib >> 5;
const chunkAi = ib & 31;
if (major !== majorType || chunkAi === 31) {
throw new Error("Indefinite-length string chunk must be a definite-length string of the same type");
}
this.off += 1;
if (++this.itemCount > MAX_ITEMS) throw new Error("CBOR item count exceeds budget");
const chunk = this.readByteString(chunkAi, depth + 1, majorType);
chunks.push(chunk);
total += chunk.length;
if (total > MAX_INDEFINITE_BYTES) {
throw new Error("Indefinite-length string exceeds size limit");
}
}
const out2 = new Uint8Array(total);
let p = 0;
for (const c of chunks) {
out2.set(c, p);
p += c.length;
}
return out2;
}
const len = this.asLength(this.readUint(ai));
const end = this.off + len;
if (end > this.bytes.length) throw new Error("CBOR byte string truncated");
const out = this.bytes.subarray(this.off, end);
this.off = end;
return out;
}
readArray(ai, depth) {
const out = [];
if (ai === 31) {
while (this.peek() !== BREAK) out.push(this.readValue(depth + 1));
this.off += 1;
return out;
}
const len = this.asLength(this.readUint(ai));
for (let i = 0; i < len; i++) out.push(this.readValue(depth + 1));
return out;
}
readMap(ai, depth) {
const out = /* @__PURE__ */ new Map();
if (ai === 31) {
while (this.peek() !== BREAK) {
const k = this.readValue(depth + 1);
out.set(k, this.readValue(depth + 1));
}
this.off += 1;
return out;
}
const len = this.asLength(this.readUint(ai));
for (let i = 0; i < len; i++) {
const k = this.readValue(depth + 1);
out.set(k, this.readValue(depth + 1));
}
return out;
}
readSimple(ai) {
switch (ai) {
case 20:
return false;
case 21:
return true;
case 22:
return null;
case 23:
return void 0;
case 24:
return { simple: this.u8() };
// one-byte simple value (32-255)
case 25:
return this.readHalfFloat();
case 26: {
const v = this.view.getFloat32(this.off);
this.off += 4;
return v;
}
case 27: {
const v = this.view.getFloat64(this.off);
this.off += 8;
return v;
}
default:
return { simple: ai };
}
}
/** IEEE 754 half-precision (binary16) → JS number. */
readHalfFloat() {
const half = this.view.getUint16(this.off);
this.off += 2;
const exp = (half & 31744) >> 10;
const frac = half & 1023;
const sign = half & 32768 ? -1 : 1;
if (exp === 0) return sign * 2 ** -14 * (frac / 1024);
if (exp === 31) return frac ? NaN : sign * Infinity;
return sign * 2 ** (exp - 15) * (1 + frac / 1024);
}
u8() {
if (this.off >= this.bytes.length) throw new Error("CBOR truncated");
const v = this.view.getUint8(this.off);
this.off += 1;
return v;
}
peek() {
if (this.off >= this.bytes.length) throw new Error("CBOR truncated");
return this.view.getUint8(this.off);
}
/** Coerce a length/tag argument to a sane JS number or throw. */
asLength(v) {
if (typeof v === "bigint" || v > 2147483647) {
throw new Error("CBOR length/tag exceeds supported range");
}
return v;
}
};
// src/bridge/packs/c2pa/x509.ts
var textDecoder2 = new TextDecoder("utf-8", { fatal: false });
var OID_COMMON_NAME = [85, 4, 3];
function parseCertificate(der) {
try {
return parseCertificateUnsafe(der);
} catch {
return {};
}
}
function parseCertificateUnsafe(der) {
const view = new DataView(der.buffer, der.byteOffset, der.byteLength);
const cert = readTlv(view, 0, der.length);
if (!cert) return {};
const tbs = readTlv(view, cert.contentStart, cert.contentEnd);
if (!tbs) return {};
const fields = readChildren(view, der, tbs.contentStart, tbs.contentEnd);
let i = 0;
if (fields[i] && fields[i]?.tag === 160) i += 1;
const issuer = fields[i + 2];
const validity = fields[i + 3];
const subject = fields[i + 4];
const info = {};
if (issuer) info.issuerCN = extractCommonName(view, der, issuer);
if (subject) info.subjectCN = extractCommonName(view, der, subject);
if (validity) {
const times = readChildren(view, der, validity.contentStart, validity.contentEnd);
if (times[0]) info.notBefore = parseTime(der, times[0]);
if (times[1]) info.notAfter = parseTime(der, times[1]);
}
return info;
}
function readTlv(view, off, limit) {
if (off + 2 > limit) return null;
const tag = view.getUint8(off);
let p = off + 1;
let len = view.getUint8(p);
p += 1;
if (len & 128) {
const n = len & 127;
if (n === 0 || n > 4) return null;
len = 0;
for (let i = 0; i < n; i++) {
len = len << 8 | view.getUint8(p);
p += 1;
}
}
const contentStart = p;
const contentEnd = p + len;
if (contentEnd > limit) return null;
return { tag, contentStart, contentEnd, end: contentEnd };
}
function readChildren(view, bytes, start, end) {
const out = [];
let off = start;
while (off < end) {
const node = readTlv(view, off, end);
if (!node) break;
out.push(node);
off = node.end;
}
return out;
}
function extractCommonName(view, bytes, name) {
for (const rdn of readChildren(view, bytes, name.contentStart, name.contentEnd)) {
for (const atv of readChildren(view, bytes, rdn.contentStart, rdn.contentEnd)) {
const parts = readChildren(view, bytes, atv.contentStart, atv.contentEnd);
const oid = parts[0];
const value = parts[1];
if (oid && value && oid.tag === 6 && oidEquals(bytes, oid, OID_COMMON_NAME)) {
return textDecoder2.decode(bytes.subarray(value.contentStart, value.contentEnd));
}
}
}
return void 0;
}
function oidEquals(bytes, node, expected) {
const len = node.contentEnd - node.contentStart;
if (len !== expected.length) return false;
for (let i = 0; i < len; i++) {
if (bytes[node.contentStart + i] !== expected[i]) return false;
}
return true;
}
function parseTime(bytes, node) {
const raw = textDecoder2.decode(bytes.subarray(node.contentStart, node.contentEnd));
const isUtc = node.tag === 23;
const m = isUtc ? /^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})?Z?$/.exec(raw) : /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})?Z?$/.exec(raw);
if (!m) return raw;
let year = Number(m[1]);
if (isUtc) year += year < 50 ? 2e3 : 1900;
const iso = `${pad(year, 4)}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6] ?? "00"}Z`;
return iso;
}
function pad(n, width) {
return String(n).padStart(width, "0");
}
// src/bridge/packs/c2pa/manifest.ts
function buildManifestStore(store) {
const manifests = store.children.map(decodeManifest);
const active = manifests[manifests.length - 1];
const result = {
manifestCount: manifests.length,
manifests
};
if (store.label !== void 0) result.label = store.label;
if (active?.label !== void 0) result.activeManifestLabel = active.label;
return result;
}
function decodeManifest(node) {
let claimNode;
let signatureNode;
let assertionStore;
for (const child of node.children) {
const label = child.label ?? "";
if (label.startsWith("c2pa.claim") || child.contentType === "c2cl") {
claimNode = child;
} else if (label === "c2pa.signature" || child.contentType === "c2cs") {
signatureNode = child;
} else if (label === "c2pa.assertions" || child.contentType === "c2as") {
assertionStore = child;
}
}
const assertions = assertionStore ? assertionStore.children.map(decodeAssertion) : [];
const manifest = { assertions };
if (node.label !== void 0) manifest.label = node.label;
if (claimNode?.cbor !== void 0) {
const claim = cborToJson(claimNode.cbor);
if (claim !== null && typeof claim === "object" && !Array.isArray(claim)) {
manifest.claim = claim;
}
}
if (signatureNode?.cbor !== void 0) {
manifest.signature = decodeCoseSignature(signatureNode.cbor);
}
return manifest;
}
function decodeAssertion(node) {
const out = {};
if (node.label !== void 0) out.label = node.label;
if (node.cbor !== void 0) {
out.data = cborToJson(node.cbor);
} else if (node.json !== void 0) {
out.data = node.json;
} else if (node.binary !== void 0) {
out.binary = node.binary;
}
return out;
}
var COSE_ALG = {
[-7]: "ES256",
[-35]: "ES384",
[-36]: "ES512",
[-8]: "EdDSA",
[-37]: "PS256",
[-38]: "PS384",
[-39]: "PS512",
[-257]: "RS256",
[-258]: "RS384",
[-259]: "RS512"
};
function decodeCoseSignature(cose) {
const arr = isCborTag(cose) ? cose.value : cose;
if (!Array.isArray(arr) || arr.length < 4) return {};
const protectedHeader = decodeProtectedHeader(arr[0]);
const unprotected = arr[1] instanceof Map ? arr[1] : /* @__PURE__ */ new Map();
const sig = {};
const algId = asInt(protectedHeader.get(1) ?? unprotected.get(1));
if (algId !== void 0) sig.alg = COSE_ALG[algId] ?? `COSE(${algId})`;
const certs = readX5Chain(protectedHeader.get(33) ?? unprotected.get(33));
if (certs.length > 0) {
sig.certChainLength = certs.length;
const first = certs[0];
if (first) {
const info = parseCertificate(first);
if (info.subjectCN !== void 0) sig.signedBy = info.subjectCN;
if (info.issuerCN !== void 0) sig.certifiedBy = info.issuerCN;
if (info.notBefore !== void 0) sig.certValidFrom = info.notBefore;
if (info.notAfter !== void 0) sig.certValidTo = info.notAfter;
}
}
return sig;
}
function decodeProtectedHeader(value) {
if (value instanceof Uint8Array && value.length > 0) {
const decoded = decodeCbor(value);
if (decoded instanceof Map) return decoded;
}
return /* @__PURE__ */ new Map();
}
function readX5Chain(value) {
if (value instanceof Uint8Array) return [value];
if (Array.isArray(value)) {
return value.filter((c) => c instanceof Uint8Array);
}
return [];
}
function asInt(value) {
if (typeof value === "number" && Number.isInteger(value)) return value;
if (typeof value === "bigint") return Number(value);
return void 0;
}
var MAX_INLINE_BYTES = 64;
function cborToJson(value) {
if (value === null || value === void 0) return value ?? null;
if (typeof value === "bigint") return value.toString();
if (value instanceof Uint8Array) return bytesToJson(value);
if (Array.isArray(value)) return value.map(cborToJson);
if (value instanceof Map) {
const obj = /* @__PURE__ */ Object.create(null);
for (const [k, v] of value) obj[keyToString(k)] = cborToJson(v);
return obj;
}
if (isCborTag(value)) {
return { $tag: value.tag, value: cborToJson(value.value) };
}
if (isCborSimple(value)) {
return { $simple: value.simple };
}
return value;
}
function bytesToJson(b) {
if (b.length <= MAX_INLINE_BYTES) return { $bytes: b.length, hex: toHex(b) };
return { $bytes: b.length, hex: `${toHex(b.subarray(0, 32))}\u2026` };
}
function keyToString(k) {
if (typeof k === "string") return k;
if (typeof k === "number" || typeof k === "bigint" || typeof k === "boolean") {
return String(k);
}
return JSON.stringify(cborToJson(k));
}
function toHex(b) {
let s = "";
for (let i = 0; i < b.length; i++) {
const byte = b[i];
if (byte === void 0) break;
s += byte.toString(16).padStart(2, "0");
}
return s;
}
// src/bridge/packs/c2pa/jumbf.ts
var textDecoder3 = new TextDecoder("utf-8", { fatal: false });
var MAX_JUMBF_BOXES = 1e4;
var MAX_APP11_PACKETS = 1024;
var MAX_FRAGMENTS_PER_INSTANCE = 256;
function detectImageFormat(bytes) {
if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
return "jpeg";
}
if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10) {
return "png";
}
return "unknown";
}
function extractC2paJumbf(bytes) {
switch (detectImageFormat(bytes)) {
case "jpeg":
return extractFromJpeg(bytes);
case "png":
return extractFromPng(bytes);
default:
return null;
}
}
function parseManifestStore(jumbf) {
const view = new DataView(jumbf.buffer, jumbf.byteOffset, jumbf.byteLength);
const top = readBox(view, jumbf, 0, jumbf.length);
if (!top || top.type !== "jumb") return null;
return parseSuperbox(view, jumbf, top.payloadStart, top.payloadEnd, 0, {
count: 0
});
}
function extractFromJpeg(bytes) {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
if (view.getUint16(0) !== 65496) return null;
const packets = /* @__PURE__ */ new Map();
let app11Count = 0;
let off = 2;
while (off + 2 <= bytes.length) {
if (view.getUint8(off) !== 255) break;
let marker = view.getUint8(off + 1);
while (marker === 255 && off + 2 < bytes.length) {
off += 1;
marker = view.getUint8(off + 1);
}
off += 2;
if (marker === 217 || marker === 218) break;
if (marker === 1 || marker >= 208 && marker <= 215) continue;
if (off + 2 > bytes.length) break;
const segLen = view.getUint16(off);
const segStart = off + 2;
const segEnd = off + segLen;
if (segLen < 2 || segEnd > bytes.length) break;
if (marker === 235 && segEnd - segStart >= 8) {
if (view.getUint16(segStart) === 19024) {
if (++app11Count > MAX_APP11_PACKETS) break;
const en = view.getUint16(segStart + 2);
const z = view.getUint32(segStart + 4);
const data = bytes.subarray(segStart + 8, segEnd);
const arr = packets.get(en) ?? [];
if (arr.length < MAX_FRAGMENTS_PER_INSTANCE) {
arr.push({ z, data });
packets.set(en, arr);
}
}
}
off = segEnd;
}
for (const arr of packets.values()) {
arr.sort((a, b) => a.z - b.z);
const first = arr[0];
if (!first) continue;
let headerLen = 8;
if (first.data.length >= 4) {
const fv = new DataView(
first.data.buffer,
first.data.byteOffset,
first.data.byteLength
);
if (fv.getUint32(0) === 1) headerLen = 16;
}
const pieces = [];
let total = 0;
for (let i = 0; i < arr.length; i++) {
const pk = arr[i];
if (!pk) continue;
const piece = i === 0 ? pk.data : pk.data.subarray(headerLen);
pieces.push(piece);
total += piece.length;
}
const buf = new Uint8Array(total);
let p = 0;
for (const piece of pieces) {
buf.set(piece, p);
p += piece.length;
}
if (isC2paStore(buf)) return buf;
}
return null;
}
function extractFromPng(bytes) {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
let off = 8;
while (off + 8 <= bytes.length) {
const len = view.getUint32(off);
const type = ascii(bytes, off + 4, 4);
const dataStart = off + 8;
const dataEnd = dataStart + len;
if (dataEnd + 4 > bytes.length) break;
if (type === "caBX") {
const buf = bytes.subarray(dataStart, dataEnd);
return isC2paStore(buf) ? buf : null;
}
if (type === "IEND") break;
off = dataEnd + 4;
}
return null;
}
function readBox(view, bytes, offset, limit) {
if (offset + 8 > limit) return null;
let len = view.getUint32(offset);
const type = ascii(bytes, offset + 4, 4);
let headerLen = 8;
if (len === 1) {
if (offset + 16 > limit) return null;
const hi = view.getUint32(offset + 8);
const lo = view.getUint32(offset + 12);
len = hi * 4294967296 + lo;
headerLen = 16;
} else if (len === 0) {
len = limit - offset;
}
const boxEnd = offset + len;
if (len < headerLen || boxEnd > limit) return null;
return { type, payloadStart: offset + headerLen, payloadEnd: boxEnd, boxEnd };
}
function parseSuperbox(view, bytes, start, end, depth, counter) {
const node = { contentType: "", children: [] };
if (depth > 64) return node;
let off = start;
const first = readBox(view, bytes, off, end);
if (first && first.type === "jumd") {
if (++counter.count > MAX_JUMBF_BOXES) return node;
parseDescription(view, bytes, first.payloadStart, first.payloadEnd, node);
off = first.boxEnd;
}
while (off < end) {
if (++counter.count > MAX_JUMBF_BOXES) return node;
const box = readBox(view, bytes, off, end);
if (!box) break;
switch (box.type) {
case "jumb":
node.children.push(
parseSuperbox(view, bytes, box.payloadStart, box.payloadEnd, depth + 1, counter)
);
break;
case "cbor":
try {
node.cbor = decodeCbor(bytes.subarray(box.payloadStart, box.payloadEnd));
} catch {
}
break;
case "json":
try {
node.json = JSON.parse(
textDecoder3.decode(bytes.subarray(box.payloadStart, box.payloadEnd))
);
} catch {
}
break;
case "bidb":
// embedded-file data box
case "uuid":
node.binary = { size: box.payloadEnd - box.payloadStart };
break;
default:
break;
}
off = box.boxEnd;
}
return node;
}
function parseDescription(view, bytes, start, end, node) {
if (start + 17 > end) return;
node.contentType = ascii(bytes, start, 4);
const toggles = view.getUint8(start + 16);
let off = start + 17;
if (toggles & 2) {
let z = off;
while (z < end && view.getUint8(z) !== 0) z += 1;
node.label = textDecoder3.decode(bytes.subarray(off, z));
}
}
function isC2paStore(buf) {
if (buf.length < 16) return false;
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
const top = readBox(view, buf, 0, buf.length);
if (!top || top.type !== "jumb") return false;
const desc = readBox(view, buf, top.payloadStart, top.payloadEnd);
if (!desc || desc.type !== "jumd") return false;
return ascii(buf, desc.payloadStart, 4) === "c2pa";
}
function ascii(bytes, off, len) {
let s = "";
for (let i = 0; i < len; i++) {
const c = bytes[off + i];
if (c === void 0) break;
s += String.fromCharCode(c);
}
return s;
}
// src/bridge/packs/c2pa/c2pa-executor.ts
var MAX_SCAN_IMAGES = 100;
var MAX_IMAGE_BYTES = 64 * 1024 * 1024;
var FETCH_TIMEOUT_MS = 15e3;
var SCAN_CONCURRENCY = 6;
var SCAN_BUDGET_MS = 3e4;
var NO_VERIFICATION_NOTE = "Manifest decoded only. Cryptographic signature, certificate-chain, and trust-list verification were NOT performed; treat provenance as unverified claims.";
async function scanImages(limit, signal) {
const allSources = collectImageSources();
const capped = Math.min(limit, MAX_SCAN_IMAGES);
const sources = allSources.slice(0, capped);
const deadline = anySignal(signal, AbortSignal.timeout(SCAN_BUDGET_MS));
const results = await mapWithConcurrency(
sources,
SCAN_CONCURRENCY,
(src) => scanOne(src, deadline)
);
const withC2pa = results.filter((r) => r.hasC2pa).length;
return {
imageCount: allSources.length,
scanned: results.length,
withC2pa,
results
};
}
async function scanOne(src, signal) {
try {
const bytes = await fetchImageBytes(src, signal);
const analysis = analyzeBytes(bytes);
if (!analysis.store) {
return { src, hasC2pa: false, format: analysis.format };
}
const active = activeManifest(analysis.store);
const summary = {
src,
hasC2pa: true,
format: analysis.format,
manifestCount: analysis.store.manifestCount,
signatureVerified: false
};
const claimGenerator = readClaimGenerator(active?.claim);
const title = readTitle(active?.claim);
if (claimGenerator !== void 0) summary.claimGenerator = claimGenerator;
if (title !== void 0) summary.title = title;
if (active?.signature?.signedBy !== void 0) {
summary.signedBy = active.signature.signedBy;
}
return summary;
} catch (err) {
return { src, hasC2pa: false, error: errorMessage(err) };
}
}
async function inspectImage(opts, signal) {
const isUrlMode = Boolean(opts.url);
const src = resolveSource(opts);
const bytes = await fetchImageBytes(src, signal, isUrlMode);
const analysis = analyzeBytes(bytes);
if (!analysis.store) {
return {
src,
hasC2pa: false,
format: analysis.format,
message: analysis.format === "unknown" ? "Unsupported image format (only JPEG and PNG are parsed today)." : "No C2PA manifest found in this image."
};
}
return {
src,
hasC2pa: true,
format: analysis.format,
manifestStore: analysis.store,
validation: { signatureVerified: false, note: NO_VERIFICATION_NOTE }
};
}
function analyzeBytes(bytes) {
const format = detectImageFormat(bytes);
const jumbf = extractC2paJumbf(bytes);
if (!jumbf) return { format };
const tree = parseManifestStore(jumbf);
if (!tree) return { format };
return { format, store: buildManifestStore(tree) };
}
function collectImageSources() {
const seen = /* @__PURE__ */ new Set();
const out = [];
for (const img of Array.from(document.querySelectorAll("img"))) {
const el = img;
const src = el.currentSrc || el.src;
if (src && !seen.has(src)) {
seen.add(src);
out.push(src);
}
}
return out;
}
function resolveSource(opts) {
if (opts.url) {
const resolved = new URL(opts.url, document.location.href);
if (resolved.origin !== document.location.origin) {
throw new Error("URL must be same-origin.");
}
return resolved.toString();
}
if (opts.selector) {
const el = document.querySelector(opts.selector);
if (!el) throw new Error(`No element matches selector: ${opts.selector}`);
if (!(el instanceof HTMLImageElement)) {
throw new Error(`Element is not an
: ${opts.selector}`);
}
const src = el.currentSrc || el.src;
if (!src) throw new Error(`
has no resolved src: ${opts.selector}`);
return src;
}
throw new Error("Provide either a `selector` or a `url`.");
}
async function fetchImageBytes(src, signal, noRedirect = false) {
const combined = anySignal(signal, AbortSignal.timeout(FETCH_TIMEOUT_MS));
let res;
try {
res = await fetch(src, {
signal: combined,
// Only block redirects when the agent supplied the URL.
// Scan/selector mode fetches already-resolved currentSrc URLs.
...noRedirect ? { redirect: "error" } : {}
});
} catch (err) {
if (err instanceof DOMException && err.name === "TimeoutError") {
throw new Error("Image fetch timed out");
}
if (err instanceof DOMException && err.name === "AbortError") {
throw new Error("Image fetch aborted");
}
throw err;
}
if (!res.ok) throw new Error("Image fetch failed.");
const length = Number(res.headers.get("content-length") ?? "0");
if (length > MAX_IMAGE_BYTES) {
throw new Error(`Image too large to inspect (${length} bytes)`);
}
if (!res.body) throw new Error("Response has no body stream");
const reader = res.body.getReader();
const chunks = [];
let total = 0;
for (; ; ) {
const { done, value } = await reader.read();
if (done) break;
total += value.byteLength;
if (total > MAX_IMAGE_BYTES) {
await reader.cancel();
throw new Error(`Image too large to inspect (${total} bytes)`);
}
chunks.push(value);
}
const assembled = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
assembled.set(chunk, offset);
offset += chunk.byteLength;
}
return assembled;
}
function activeManifest(store) {
return store.manifests[store.manifests.length - 1];
}
function readClaimGenerator(claim) {
if (!claim) return void 0;
const info = claim["claim_generator_info"];
if (Array.isArray(info) && info.length > 0) {
const first = info[0];
if (first && typeof first === "object") {
const name = first["name"];
const version = first["version"];
if (typeof name === "string") {
return typeof version === "string" ? `${name} ${version}` : name;
}
}
}
const generator = claim["claim_generator"];
return typeof generator === "string" ? generator : void 0;
}
function readTitle(claim) {
if (!claim) return void 0;
const title = claim["dc:title"] ?? claim["title"];
return typeof title === "string" ? title : void 0;
}
function errorMessage(err) {
return err instanceof Error ? err.message : String(err);
}
function anySignal(optional, required) {
return optional ? AbortSignal.any([optional, required]) : required;
}
async function mapWithConcurrency(items, concurrency, fn) {
const results = new Array(items.length);
let next = 0;
const workers = [];
const limit = Math.max(1, Math.min(concurrency, items.length));
for (let w = 0; w < limit; w++) {
workers.push(
(async () => {
for (; ; ) {
const i = next++;
if (i >= items.length) return;
const item = items[i];
if (item === void 0) continue;
results[i] = await fn(item);
}
})()
);
}
await Promise.all(workers);
return results;
}
// src/bridge/packs/c2pa/index.ts
var TOOLS = [
{
name: "scan_images_c2pa",
description: "Scan every
on the current page for C2PA Content Credentials (content provenance metadata) and return a per-image summary: whether a manifest is present, the claim generator (the tool that produced or edited the image), the title, and the signer name. Use this to find which images on a page carry provenance data. Note: provenance is decoded but NOT cryptographically verified.",
inputSchema: {
type: "object",
properties: {
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 25,
description: "Maximum number of images to fetch and scan (default 25, max 100)."
}
},
required: []
}
},
{
name: "inspect_image_c2pa",
description: "Fetch a single image (by CSS selector or URL) and return its fully decoded C2PA manifest store: every manifest's claim, assertions (e.g. c2pa.actions edit history, schema.org authorship), and the COSE signature details (algorithm and signing-certificate identity). Provide exactly one of `selector` or `url`. Note: the signature is decoded but NOT cryptographically verified.",
inputSchema: {
type: "object",
properties: {
selector: {
type: "string",
maxLength: 8192,
description: "CSS selector for an
element on the page."
},
url: {
type: "string",
maxLength: 8192,
description: "Same-origin image URL (absolute same-origin, or relative to the current page) to fetch and inspect. Cross-origin URLs are rejected."
}
},
required: []
}
}
];
function optionalInt(args, key, min, max, fallback) {
if (typeof args !== "object" || args === null) return fallback;
const v = args[key];
if (v === void 0) return fallback;
if (typeof v !== "number" || !Number.isInteger(v)) return null;
if (v < min || v > max) return null;
return v;
}
var MAX_STRING_ARG = 8192;
function readStringArg(args, key) {
if (typeof args !== "object" || args === null) return { kind: "absent" };
const v = args[key];
if (v === void 0 || v === null) return { kind: "absent" };
if (typeof v !== "string") {
return { kind: "invalid", reason: `\`${key}\` must be a string.` };
}
if (v.length === 0) return { kind: "absent" };
if (v.length > MAX_STRING_ARG) {
return {
kind: "invalid",
reason: `\`${key}\` exceeds the ${MAX_STRING_ARG}-character limit.`
};
}
return { kind: "ok", value: v };
}
var handleScanImages = async (rawArgs, ctx) => {
const limit = optionalInt(rawArgs, "limit", 1, 100, 25);
if (limit === null) return errorResult("Invalid tool arguments");
return ok({ ...await scanImages(limit, ctx.signal) });
};
var handleInspectImage = async (rawArgs, ctx) => {
const selector = readStringArg(rawArgs, "selector");
const url = readStringArg(rawArgs, "url");
if (selector.kind === "invalid") return errorResult(selector.reason);
if (url.kind === "invalid") return errorResult(url.reason);
const hasSelector = selector.kind === "ok";
const hasUrl = url.kind === "ok";
if (!hasSelector && !hasUrl) {
return errorResult("Provide either `selector` or `url`.");
}
if (hasSelector && hasUrl) {
return errorResult("Provide only one of `selector` or `url`, not both.");
}
const result = await inspectImage(
{
...selector.kind === "ok" ? { selector: selector.value } : {},
...url.kind === "ok" ? { url: url.value } : {}
},
ctx.signal
);
return ok({ ...result });
};
function wrapErrors(h) {
return async (args, ctx) => {
try {
return await h(args, ctx);
} catch (err) {
return errorResult(err instanceof Error ? err.message : String(err));
}
};
}
var c2paPack = {
kind: "static",
name: "c2pa",
tools: TOOLS,
handlers: {
scan_images_c2pa: wrapErrors(handleScanImages),
inspect_image_c2pa: wrapErrors(handleInspectImage)
}
};
// src/bridge/packs/mcp-server-client/mcp-http-utils.ts
var MCP_TIMEOUT_MS = 2e4;
var MCP_MAX_BYTES = 8 * 1024 * 1024;
var MCP_MAX_RESPONSE_CHUNKS = 65536;
var MCP_MAX_LIST_PAGES = 20;
var MCP_MAX_TOOLS = 200;
var MCP_MAX_TOOL_NAME_CHARS = 128;
var MCP_MAX_DESCRIPTION_CHARS = 4096;
var MCP_MAX_SCHEMA_BYTES = 32 * 1024;
var MCP_MAX_TOOL_LIST_BYTES = 2 * 1024 * 1024;
var MCP_MAX_STRUCTURED_CONTENT_BYTES = 64 * 1024;
var MCP_MAX_CONTENT_TEXT_BYTES = 64 * 1024;
var MCP_MAX_CONTENT_TEXT_TOTAL_BYTES = 256 * 1024;
var RPC_ID = 1;
var MCP_CLIENT_INFO = {
name: "cloudflare-webmcp-bridge",
version: "0.0.1"
};
var encoder = new TextEncoder();
var DISALLOWED_FORMAT_CONTROL_CLASS = "[\\u202A-\\u202E\\u2066-\\u2069\\u200E\\u200F\\u061C\\u200B-\\u200D\\u2060\\uFEFF\\u{E0000}-\\u{E007F}]";
var DISALLOWED_FORMAT_CONTROLS_ALL_REGEX = new RegExp(
DISALLOWED_FORMAT_CONTROL_CLASS,
"gu"
);
var DISALLOWED_FORMAT_CONTROLS_ANY_REGEX = new RegExp(
DISALLOWED_FORMAT_CONTROL_CLASS,
"u"
);
function stripInvisibleControls(text) {
return text.replace(DISALLOWED_FORMAT_CONTROLS_ALL_REGEX, "");
}
function formatToolNameForLog(name) {
return JSON.stringify(name).replace(
DISALLOWED_FORMAT_CONTROLS_ALL_REGEX,
(character) => {
const codePoint = character.codePointAt(0);
return codePoint === void 0 ? "" : `\\u{${codePoint.toString(16).toUpperCase()}}`;
}
);
}
function hasInvisibleControls(text) {
return DISALLOWED_FORMAT_CONTROLS_ANY_REGEX.test(text);
}
var McpHttpError = class extends Error {
constructor(status, rpcError) {
super(rpcError?.message ?? `MCP endpoint returned HTTP ${status}`);
this.status = status;
this.rpcError = rpcError;
}
status;
rpcError;
_tag = "McpHttpError";
};
function isObject(v) {
return typeof v === "object" && v !== null && !Array.isArray(v);
}
function sliceUtf8Bytes(s, maxBytes) {
const bytes = encoder.encode(s);
if (bytes.length <= maxBytes) return s;
let end = maxBytes;
while (end > 0) {
const b = bytes[end];
if (b === void 0 || (b & 192) !== 128) break;
end--;
}
return new TextDecoder("utf-8", { fatal: false }).decode(
bytes.slice(0, end)
);
}
async function readBoundedText(res) {
const body = res.body;
if (!body) return res.text();
const reader = body.getReader();
const chunks = [];
let total = 0;
let done = false;
try {
for (let count = 0; count < MCP_MAX_RESPONSE_CHUNKS; count++) {
const { done: streamDone, value } = await reader.read();
if (streamDone) {
done = true;
break;
}
if (value) {
total += value.byteLength;
if (total > MCP_MAX_BYTES) {
throw new Error(
`MCP response exceeded ${MCP_MAX_BYTES} bytes; aborting.`
);
}
chunks.push(value);
}
}
if (!done) throw new Error("MCP response exceeded chunk limit; aborting.");
} finally {
await reader.cancel().catch(() => {
});
}
const merged = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
merged.set(chunk, offset);
offset += chunk.byteLength;
}
return new TextDecoder().decode(merged);
}
function callSignal(signal) {
const timeout = AbortSignal.timeout(MCP_TIMEOUT_MS);
return signal ? AbortSignal.any([signal, timeout]) : timeout;
}
function combineSignals(signal, lifecycleSignal) {
return signal ? AbortSignal.any([signal, lifecycleSignal]) : lifecycleSignal;
}
async function waitFor(pending, signal) {
if (!signal) return pending;
if (signal.aborted) throw signal.reason;
return new Promise((resolve2, reject) => {
const onAbort = () => reject(signal.reason);
signal.addEventListener("abort", onAbort, { once: true });
pending.then(
(value) => {
signal.removeEventListener("abort", onAbort);
resolve2(value);
},
(error) => {
signal.removeEventListener("abort", onAbort);
reject(error);
}
);
});
}
function assertSameOrigin(endpoint, origin) {
if (!origin) return endpoint;
try {
const url = new URL(endpoint, origin);
if (url.origin !== origin) {
throw new Error(
`MCP endpoint "${endpoint}" is cross-origin; only same-origin endpoints are allowed.`
);
}
return url.toString();
} catch (error) {
if (error instanceof Error && error.message.includes("cross-origin")) {
throw error;
}
throw new Error(`MCP endpoint "${endpoint}" is not a valid URL.`);
}
}
function requestHeaders(protocolVersion, sessionId) {
const headers = {
"content-type": "application/json",
accept: "application/json, text/event-stream",
// Distinguish agent-mediated calls from user-initiated ones so the
// site can attribute actions and apply different policy.
"x-webmcp-agent": "1"
};
if (protocolVersion) headers["MCP-Protocol-Version"] = protocolVersion;
if (sessionId) headers["MCP-Session-Id"] = sessionId;
return headers;
}
async function mcpRpcResponse(endpoint, origin, method, params, options = {}) {
const rpcId = options.rpcId ?? RPC_ID;
const res = await fetch(assertSameOrigin(endpoint, origin), {
method: "POST",
headers: options.headers ?? requestHeaders(),
credentials: "same-origin",
redirect: "error",
body: JSON.stringify({
jsonrpc: "2.0",
id: rpcId,
method,
params: options.requestParams ?? params
}),
signal: callSignal(options.signal)
});
if (!res.ok) {
let rpcError;
const raw = await readBoundedText(res);
if (raw) {
try {
const parsed = JSON.parse(raw);
if (isObject(parsed) && isResponseEnvelope(parsed, rpcId)) {
const error = parsed["error"];
if (isJsonRpcError(error)) rpcError = error;
}
} catch {
}
}
throw new McpHttpError(res.status, rpcError);
}
const contentType = res.headers.get("content-type") ?? "";
const rpc = contentType.includes("text/event-stream") ? await readEventStreamResponse(res, rpcId) : parseJson(await readBoundedText(res));
if (!isResponseEnvelope(rpc, rpcId)) {
throw new Error("MCP endpoint returned a mismatched JSON-RPC response");
}
return { result: extractResult(rpc), headers: res.headers };
}
async function mcpRpcNotification(endpoint, origin, method, signal, protocolVersion, sessionId) {
const res = await fetch(assertSameOrigin(endpoint, origin), {
method: "POST",
headers: requestHeaders(protocolVersion, sessionId),
credentials: "same-origin",
redirect: "error",
body: JSON.stringify({ jsonrpc: "2.0", method }),
signal: callSignal(signal)
});
if (!res.ok) throw new McpHttpError(res.status);
await res.body?.cancel().catch(() => {
});
}
async function listTools(rpc, signal) {
const out = [];
let totalBytes = 0;
let cursor;
for (let page = 0; page < MCP_MAX_LIST_PAGES; page++) {
const result = await rpc("tools/list", cursor ? { cursor } : {}, signal);
if (!Array.isArray(result.tools)) {
throw new Error("MCP tools/list response has no tools array");
}
for (const tool of result.tools) {
const parsed = toToolDef(tool);
if (!parsed) continue;
const { definition, bytes } = parsed;
if (totalBytes + bytes > MCP_MAX_TOOL_LIST_BYTES) {
console.warn(
`[webmcp-interceptor] MCP tools/list exceeds ${MCP_MAX_TOOL_LIST_BYTES} bytes of tool definitions; stopping discovery early.`
);
return out;
}
totalBytes += bytes;
out.push(definition);
if (out.length >= MCP_MAX_TOOLS) {
console.warn(
`[webmcp-interceptor] MCP endpoint returned >= ${MCP_MAX_TOOLS} tools; stopping discovery early.`
);
return out;
}
}
cursor = typeof result.nextCursor === "string" && result.nextCursor.length > 0 ? result.nextCursor : void 0;
if (!cursor) break;
}
return out;
}
function toToolDef(value) {
if (!isObject(value)) return null;
if (typeof value.name !== "string" || value.name.trim().length === 0) return null;
if (hasInvisibleControls(value.name)) {
console.warn(
`[webmcp-interceptor] mcp-server-client: skipping tool ${formatToolNameForLog(value.name)} because its name contains bidi or invisible controls.`
);
return null;
}
if (value.name.length > MCP_MAX_TOOL_NAME_CHARS) {
console.warn(
`[webmcp-interceptor] mcp-server-client: skipping tool ${formatToolNameForLog(value.name.slice(0, 32) + "\u2026")} because its name exceeds ${MCP_MAX_TOOL_NAME_CHARS} characters.`
);
return null;
}
const definition = { name: value.name };
if (typeof value.description === "string" && value.description.trim().length > 0) {
definition.description = value.description.length > MCP_MAX_DESCRIPTION_CHARS ? value.description.slice(0, MCP_MAX_DESCRIPTION_CHARS) : value.description;
}
if (isObject(value.inputSchema) && value.inputSchema["type"] === "object") {
const schemaBytes = serializedBytes(value.inputSchema);
if (schemaBytes === void 0 || schemaBytes > MCP_MAX_SCHEMA_BYTES) {
console.warn(
`[webmcp-interceptor] mcp-server-client: skipping tool ${formatToolNameForLog(value.name)} because its inputSchema is unserializable or exceeds ${MCP_MAX_SCHEMA_BYTES} bytes.`
);
return null;
}
definition.inputSchema = value.inputSchema;
}
const bytes = serializedBytes(definition);
if (bytes === void 0) return null;
return { definition, bytes };
}
function serializedBytes(value) {
try {
return encoder.encode(JSON.stringify(value)).length;
} catch {
return void 0;
}
}
function toCallResult(result) {
if (!Array.isArray(result.content)) {
throw new Error("MCP tools/call result has no content array");
}
let textTotal = 0;
const cappedContent = result.content.filter(isContentBlock).map((block) => {
if (isObject(block) && block["type"] === "text") {
const text = block["text"];
if (typeof text === "string") {
const remaining = MCP_MAX_CONTENT_TEXT_TOTAL_BYTES - textTotal;
if (remaining <= 0) return { type: "text", text: "" };
let capped = text;
if (encoder.encode(capped).length > MCP_MAX_CONTENT_TEXT_BYTES) {
capped = sliceUtf8Bytes(capped, MCP_MAX_CONTENT_TEXT_BYTES);
}
if (encoder.encode(capped).length > remaining) {
capped = sliceUtf8Bytes(capped, remaining);
}
textTotal += encoder.encode(capped).length;
return { type: "text", text: capped };
}
}
return block;
});
const out = {
content: cappedContent,
isError: result.isError === true
};
if (isObject(result.structuredContent)) {
try {
const size = encoder.encode(JSON.stringify(result.structuredContent)).length;
if (size <= MCP_MAX_STRUCTURED_CONTENT_BYTES) {
out.structuredContent = result.structuredContent;
} else {
console.warn(
`[webmcp-interceptor] mcp-server-client: structuredContent exceeds ${MCP_MAX_STRUCTURED_CONTENT_BYTES} bytes; dropping.`
);
}
} catch {
}
}
return out;
}
function isContentBlock(value) {
if (!isObject(value) || typeof value["type"] !== "string") return false;
if (value["type"] === "text") return typeof value["text"] === "string";
return true;
}
function parseJson(raw) {
try {
return JSON.parse(raw);
} catch {
throw new Error("MCP endpoint returned a non-JSON response");
}
}
async function readEventStreamResponse(res, rpcId = RPC_ID) {
const body = res.body;
if (!body) return parseEventStream(await res.text());
const reader = body.getReader();
const decoder = new TextDecoder();
let tail = "";
let total = 0;
try {
for (; ; ) {
const { done, value } = await reader.read();
if (done) break;
if (!value) continue;
total += value.byteLength;
if (total > MCP_MAX_BYTES) {
throw new Error(`MCP response exceeded ${MCP_MAX_BYTES} bytes; aborting.`);
}
const chunk = decoder.decode(value, { stream: true });
let position = 0;
let newline;
while ((newline = chunk.indexOf("\n", position)) >= 0) {
const rawLine = tail + chunk.slice(position, newline);
tail = "";
position = newline + 1;
const envelope2 = tryParseDataLine(rawLine, rpcId);
if (envelope2 !== void 0) return envelope2;
}
tail += chunk.slice(position);
}
const envelope = tryParseDataLine(tail, rpcId);
if (envelope !== void 0) return envelope;
} finally {
await reader.cancel().catch(() => {
});
}
throw new Error("MCP event stream contained no JSON-RPC response");
}
function parseEventStream(raw) {
for (const rawLine of raw.split(/\r?\n/)) {
const envelope = tryParseDataLine(rawLine, RPC_ID);
if (envelope !== void 0) return envelope;
}
throw new Error("MCP event stream contained no JSON-RPC response");
}
function tryParseDataLine(rawLine, rpcId) {
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
if (!line.startsWith("data:")) return void 0;
const payload = line.slice("data:".length).trim();
if (!payload) return void 0;
try {
const parsed = JSON.parse(payload);
if (isResponseEnvelope(parsed, rpcId)) return parsed;
} catch {
}
return void 0;
}
function isResponseEnvelope(value, rpcId) {
if (!isObject(value) || value["jsonrpc"] !== "2.0") return false;
const id = value["id"];
return id === rpcId || id === null && isJsonRpcError(value["error"]);
}
function isJsonRpcError(value) {
return isObject(value) && typeof value["code"] === "number" && Number.isInteger(value["code"]) && typeof value["message"] === "string";
}
function extractResult(rpc) {
if (!isObject(rpc)) {
throw new Error("Malformed JSON-RPC response from MCP endpoint");
}
if ("error" in rpc) {
const error = rpc.error;
if (!isJsonRpcError(error)) {
throw new Error("MCP endpoint returned a malformed JSON-RPC error");
}
throw new Error(`MCP endpoint error: ${error["message"]}`);
}
const result = rpc.result;
if (!isObject(result)) throw new Error("MCP response missing result");
return result;
}
async function terminateMcpSession(endpoint, origin, protocolVersion, sessionId) {
try {
await fetch(assertSameOrigin(endpoint, origin), {
method: "DELETE",
headers: requestHeaders(protocolVersion, sessionId),
credentials: "same-origin",
redirect: "error",
keepalive: true,
signal: AbortSignal.timeout(MCP_TIMEOUT_MS)
});
} catch {
}
}
// src/bridge/packs/mcp-server-client/initialized-client.ts
var MCP_INITIALIZE_MAX_ATTEMPTS = 2;
var MCP_MAX_SESSION_ID_LENGTH = 512;
var MCP_SUPPORTED_PROTOCOL_VERSIONS = [
"2025-11-25",
"2025-06-18",
"2025-03-26"
];
var InitializedClient = class {
constructor(options) {
this.options = options;
}
options;
protocolVersion;
sessionId;
initializing;
get negotiatedProtocolVersion() {
return this.protocolVersion;
}
async connect(signal) {
await this.ensureInitialized(signal);
}
async listTools(signal) {
return listTools(
(method, params, requestSignal) => this.sessionRpc(method, params, requestSignal),
signal
);
}
async callTool(name, args, signal) {
return toCallResult(
await this.sessionRpc("tools/call", { name, arguments: args }, signal)
);
}
async close() {
const sessionId = this.sessionId;
const protocolVersion = this.protocolVersion;
this.clearSession();
if (!sessionId || !protocolVersion) return;
await terminateMcpSession(
this.options.endpoint,
this.options.origin,
protocolVersion,
sessionId
);
}
async ensureInitialized(signal) {
this.options.assertOpen();
if (this.protocolVersion) return;
this.initializing ??= this.initialize().finally(() => {
this.initializing = void 0;
});
await waitFor(this.initializing, signal);
this.options.assertOpen();
}
async initialize() {
for (let attempt = 0; attempt < MCP_INITIALIZE_MAX_ATTEMPTS; attempt++) {
this.options.assertOpen();
const response = await mcpRpcResponse(
this.options.endpoint,
this.options.origin,
"initialize",
{
protocolVersion: MCP_SUPPORTED_PROTOCOL_VERSIONS[0],
capabilities: {},
clientInfo: MCP_CLIENT_INFO
},
{
signal: this.options.lifecycleSignal,
rpcId: this.options.nextRpcId()
}
);
const sessionId = readSessionId(response.headers);
const protocolVersion = response.result["protocolVersion"];
try {
this.options.assertOpen();
} catch (error) {
if (sessionId) {
void terminateMcpSession(
this.options.endpoint,
this.options.origin,
typeof protocolVersion === "string" ? protocolVersion : MCP_SUPPORTED_PROTOCOL_VERSIONS[0],
sessionId
);
}
throw error;
}
try {
if (typeof protocolVersion !== "string" || !MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {
throw new Error(
`MCP server negotiated unsupported protocol version: ${String(protocolVersion)}`
);
}
const capabilities = response.result["capabilities"];
if (!isObject(capabilities) || !isObject(capabilities["tools"])) {
throw new Error("MCP server does not advertise the tools capability");
}
} catch (error) {
if (sessionId) {
void terminateMcpSession(
this.options.endpoint,
this.options.origin,
typeof protocolVersion === "string" ? protocolVersion : MCP_SUPPORTED_PROTOCOL_VERSIONS[0],
sessionId
);
}
throw error;
}
try {
await mcpRpcNotification(
this.options.endpoint,
this.options.origin,
"notifications/initialized",
this.options.lifecycleSignal,
protocolVersion,
sessionId
);
this.options.assertOpen();
this.protocolVersion = protocolVersion;
this.sessionId = sessionId;
return;
} catch (error) {
const sessionMissing = error instanceof McpHttpError && error.status === 404 && sessionId !== void 0;
if (sessionMissing && attempt + 1 < MCP_INITIALIZE_MAX_ATTEMPTS) continue;
if (sessionId && !sessionMissing) {
void terminateMcpSession(
this.options.endpoint,
this.options.origin,
protocolVersion,
sessionId
);
}
throw error;
}
}
}
async sessionRpc(method, params, signal) {
await this.ensureInitialized(signal);
this.options.assertOpen();
const sessionId = this.sessionId;
try {
return await this.rpcWithCurrentSession(method, params, signal);
} catch (error) {
if (!(error instanceof McpHttpError) || error.status !== 404 || !sessionId) {
throw error;
}
this.options.assertOpen();
if (this.sessionId === sessionId) this.clearSession();
await this.ensureInitialized(signal);
return this.rpcWithCurrentSession(method, params, signal);
}
}
async rpcWithCurrentSession(method, params, signal) {
const protocolVersion = this.protocolVersion;
if (!protocolVersion) throw new Error("MCP client is not initialized");
this.options.assertOpen();
return (await mcpRpcResponse(
this.options.endpoint,
this.options.origin,
method,
params,
{
signal: combineSignals(signal, this.options.lifecycleSignal),
headers: requestHeaders(protocolVersion, this.sessionId),
rpcId: this.options.nextRpcId()
}
)).result;
}
clearSession() {
this.protocolVersion = void 0;
this.sessionId = void 0;
}
};
function readSessionId(headers) {
const sessionId = headers.get("MCP-Session-Id");
if (sessionId === null) return void 0;
if (sessionId.length === 0 || sessionId.length > MCP_MAX_SESSION_ID_LENGTH || !/^[\x21-\x7e]+$/.test(sessionId)) {
throw new Error("MCP endpoint returned an invalid session ID");
}
return sessionId;
}
// src/bridge/packs/mcp-server-client/per-request-client.ts
var MCP_PER_REQUEST_PROTOCOL_VERSION = "2026-07-28";
var MCP_UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE = -32022;
var MCP_MAX_HEADER_BINDINGS = 64;
var MCP_MAX_HEADER_VALUE_BYTES = 8 * 1024;
var MCP_MAX_SCHEMA_PROPERTIES = 256;
var headerEncoder = new TextEncoder();
var InitializedFallbackRequired = class extends Error {
};
var PerRequestProtocolSelectedError = class extends Error {
constructor(originalError) {
super("Per-request MCP protocol was selected before a later request failed");
this.originalError = originalError;
}
originalError;
};
var PerRequestClient = class {
constructor(options) {
this.options = options;
}
options;
// Only the compiled header bindings are retained per tool, never the
// server's description or inputSchema, so the cache stays bounded.
headerBindings = /* @__PURE__ */ new Map();
async discoverTools() {
let responseReceived = false;
try {
const tools = await listTools(
async (method, params, signal) => {
return this.rpc(method, params, signal, () => {
responseReceived = true;
});
},
this.options.lifecycleSignal
);
return this.rememberTools(tools);
} catch (error) {
if (responseReceived) throw new PerRequestProtocolSelectedError(error);
if (!responseReceived && shouldFallbackToInitializedMode(error)) {
throw new InitializedFallbackRequired();
}
throw error;
}
}
async listTools(signal) {
return this.rememberTools(
await listTools(
(method, params, requestSignal) => this.rpc(method, params, requestSignal),
signal
)
);
}
async callTool(name, args, signal) {
return toCallResult(
await this.rpc("tools/call", { name, arguments: args }, signal)
);
}
async rpc(method, params, signal, onResponse) {
this.options.assertOpen();
const toolName = typeof params["name"] === "string" ? params["name"] : void 0;
const response = await mcpRpcResponse(
this.options.endpoint,
this.options.origin,
method,
params,
{
signal: combineSignals(signal, this.options.lifecycleSignal),
rpcId: this.options.nextRpcId(),
headers: buildPerRequestHeaders(
requestHeaders(MCP_PER_REQUEST_PROTOCOL_VERSION),
method,
params,
toolName ? this.headerBindings.get(toolName) : void 0
),
requestParams: {
...params,
_meta: {
"io.modelcontextprotocol/protocolVersion": MCP_PER_REQUEST_PROTOCOL_VERSION,
"io.modelcontextprotocol/clientInfo": MCP_CLIENT_INFO,
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
);
onResponse?.();
const resultType = response.result["resultType"];
if (resultType !== void 0 && resultType !== "complete") {
throw new Error(
`MCP endpoint returned unsupported resultType: ${String(resultType)}`
);
}
return response.result;
}
rememberTools(tools) {
this.headerBindings.clear();
return tools.filter((tool) => {
const { bindings, error } = readHeaderBindings(tool.inputSchema);
if (error) {
console.warn(
`[webmcp-interceptor] mcp-server-client: skipping ${tool.name}: ${error}.`
);
return false;
}
this.headerBindings.set(tool.name, bindings);
return true;
});
}
};
function shouldFallbackToInitializedMode(error) {
if (!(error instanceof McpHttpError) || error.status !== 400) return false;
if (error.rpcError?.code !== MCP_UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE) {
return true;
}
if (!isObject(error.rpcError.data)) return true;
const supported = error.rpcError.data["supported"];
if (!Array.isArray(supported) || supported.length === 0 || !supported.every((version) => typeof version === "string")) {
return true;
}
if (supported.includes(MCP_PER_REQUEST_PROTOCOL_VERSION)) return false;
const perRequestVersions = supported.filter(
(version) => version >= MCP_PER_REQUEST_PROTOCOL_VERSION
);
if (perRequestVersions.length > 0) {
console.warn(
`[webmcp-interceptor] mcp-server-client: server advertised unsupported MCP protocol version(s) ${perRequestVersions.join(", ")}; this bridge supports ${MCP_PER_REQUEST_PROTOCOL_VERSION}.`
);
return false;
}
return true;
}
function encodeHeaderValue(value) {
const bytes = headerEncoder.encode(value);
let encoded;
if (value.length > 0 && value === value.trim() && /^[\x20-\x7e]+$/.test(value) && !(value.startsWith("=?base64?") && value.endsWith("?="))) {
encoded = value;
} else {
let binary = "";
for (const byte of bytes) binary += String.fromCharCode(byte);
encoded = `=?base64?${btoa(binary)}?=`;
}
if (encoded.length > MCP_MAX_HEADER_VALUE_BYTES) {
throw new Error("MCP header value exceeds size limit");
}
return encoded;
}
function readHeaderBindings(schema) {
if (!schema || !isObject(schema["properties"])) return { bindings: [] };
const bindings = [];
const headerNames = /* @__PURE__ */ new Set();
const properties = Object.entries(schema["properties"]);
if (properties.length > MCP_MAX_SCHEMA_PROPERTIES) {
return { bindings: [], error: "too many schema properties" };
}
for (const [argumentName, property] of properties) {
if (!isObject(property) || property["x-mcp-header"] === void 0) continue;
const headerName = property["x-mcp-header"];
const type = property["type"];
if (typeof headerName !== "string" || !/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(headerName) || type !== "string" && type !== "integer" && type !== "boolean") {
return { bindings: [], error: "invalid x-mcp-header annotation" };
}
const normalizedName = headerName.toLowerCase();
if (headerNames.has(normalizedName)) {
return { bindings: [], error: "duplicate x-mcp-header annotation" };
}
if (bindings.length >= MCP_MAX_HEADER_BINDINGS) {
return { bindings: [], error: "too many x-mcp-header annotations" };
}
headerNames.add(normalizedName);
bindings.push({ headerName, argumentName, type });
}
return { bindings };
}
function buildPerRequestHeaders(baseHeaders, method, params, bindings) {
const headers = {
...baseHeaders,
"Mcp-Method": method
};
const toolName = params["name"];
if (typeof toolName === "string") {
headers["Mcp-Name"] = encodeHeaderValue(toolName);
}
if (!bindings || !isObject(params["arguments"])) return headers;
for (const binding of bindings) {
if (!Object.hasOwn(params["arguments"], binding.argumentName)) continue;
const value = params["arguments"][binding.argumentName];
if (value === void 0 || value === null) continue;
const valid = binding.type === "string" && typeof value === "string" || binding.type === "boolean" && typeof value === "boolean" || binding.type === "integer" && typeof value === "number" && Number.isSafeInteger(value);
if (!valid) {
throw new Error(`${String(toolName)}: invalid ${binding.argumentName}`);
}
headers[`Mcp-Param-${binding.headerName}`] = encodeHeaderValue(String(value));
}
return headers;
}
// src/bridge/packs/mcp-server-client/mcp-client.ts
var McpHttpClient = class {
selectedClient;
negotiating;
pagehideHandler;
lifecycleController = new AbortController();
perRequestClient;
initializedClient;
closed = false;
rpcId = 0;
constructor(endpoint, origin) {
const options = {
endpoint,
origin,
lifecycleSignal: this.lifecycleController.signal,
nextRpcId: () => this.nextRpcId(),
assertOpen: () => this.assertOpen()
};
this.perRequestClient = new PerRequestClient(options);
this.initializedClient = new InitializedClient(options);
}
async listTools(signal) {
const initialTools = await this.ensureClient(signal);
if (initialTools) return initialTools;
return this.requireSelectedClient().listTools(signal);
}
async callTool(name, args, signal) {
await this.ensureClient(signal);
return this.requireSelectedClient().callTool(name, args, signal);
}
attachPageLifecycle() {
if (typeof window === "undefined" || this.pagehideHandler || this.closed) return;
this.pagehideHandler = (event) => {
if (event.persisted) return;
if (this.pagehideHandler) {
window.removeEventListener("pagehide", this.pagehideHandler);
this.pagehideHandler = void 0;
}
void this.close();
};
window.addEventListener("pagehide", this.pagehideHandler);
}
async close() {
if (this.closed) return;
this.closed = true;
this.lifecycleController.abort();
if (typeof window !== "undefined" && this.pagehideHandler) {
window.removeEventListener("pagehide", this.pagehideHandler);
this.pagehideHandler = void 0;
}
await this.initializedClient.close();
}
async ensureClient(signal) {
this.assertOpen();
if (this.selectedClient) return void 0;
this.negotiating ??= this.negotiate().finally(() => {
this.negotiating = void 0;
});
const tools = await waitFor(this.negotiating, signal);
this.assertOpen();
return tools;
}
async negotiate() {
try {
const tools = await this.perRequestClient.discoverTools();
this.assertOpen();
this.selectedClient = this.perRequestClient;
return tools;
} catch (error) {
if (error instanceof PerRequestProtocolSelectedError) {
this.selectedClient = this.perRequestClient;
throw error.originalError;
}
if (!(error instanceof InitializedFallbackRequired)) throw error;
await this.initializedClient.connect();
this.assertOpen();
this.selectedClient = this.initializedClient;
console.warn(
`[webmcp-interceptor] mcp-server-client: falling back from MCP ${MCP_PER_REQUEST_PROTOCOL_VERSION} to initialized MCP ${this.initializedClient.negotiatedProtocolVersion}.`
);
return void 0;
}
}
requireSelectedClient() {
if (!this.selectedClient) {
throw new Error("MCP client has not selected a protocol mode");
}
return this.selectedClient;
}
assertOpen() {
if (this.closed) throw new Error("MCP client is closed");
}
nextRpcId() {
this.rpcId += 1;
return this.rpcId;
}
};
// src/bridge/packs/mcp-server-client/index.ts
function isObject2(v) {
return typeof v === "object" && v !== null;
}
var SITE_TOOL_NOTE = "[Site-provided MCP tool \u2014 executes with your session credentials.] ";
function clampDescription(desc) {
return desc.length > MCP_MAX_DESCRIPTION_CHARS ? desc.slice(0, MCP_MAX_DESCRIPTION_CHARS) : desc;
}
function sanitiseDescription(desc, name) {
const stripped = stripInvisibleControls(desc);
if (stripped !== desc) {
console.warn(
`[webmcp-interceptor] mcp-server-client: removed bidi or invisible controls from the description of tool ${formatToolNameForLog(name)}.`
);
}
return clampDescription(stripped.trim().length > 0 ? stripped : name);
}
function safeInputSchema(schema) {
if (!isObject2(schema) || schema["type"] !== "object") {
return { type: "object", properties: {} };
}
return schema;
}
function makeHandler(client, toolName) {
return async (rawArgs, ctx) => {
const args = isObject2(rawArgs) ? rawArgs : {};
try {
const result = await client.callTool(toolName, args, ctx.signal);
const out = {
content: result.content
};
if (isObject2(result.structuredContent)) {
out.structuredContent = result.structuredContent;
}
if (result.isError) out.isError = true;
return customResult(out);
} catch (err) {
return errorResult(err instanceof Error ? err.message : String(err));
}
};
}
async function resolve(ctx) {
const endpoint = ctx.mcpUrl;
if (!endpoint) {
return { tools: [], handlers: {} };
}
let defs;
const client = new McpHttpClient(endpoint, ctx.origin);
client.attachPageLifecycle();
try {
defs = await client.listTools(ctx.signal);
} catch (err) {
console.warn(
`[webmcp-interceptor] mcp-server-client: tools/list failed for "${endpoint}"; registering no site tools.`,
err
);
void client.close();
return { tools: [], handlers: {} };
}
if (defs.length === 0) {
void client.close();
return { tools: [], handlers: {} };
}
const tools = [];
const handlers = {};
for (const def of defs) {
tools.push({
name: def.name,
// Prepend a provenance note so the agent can distinguish site-
// provided tools from built-ins, and cap the description length so
// a hostile endpoint can't flood the agent via an oversized
// description.
description: SITE_TOOL_NOTE + sanitiseDescription(def.description || def.name, def.name),
inputSchema: safeInputSchema(def.inputSchema)
});
handlers[def.name] = makeHandler(client, def.name);
}
console.debug(
`[webmcp-interceptor] mcp-server-client: registered ${tools.length} site tool(s) from ${endpoint}.`
);
return { tools, handlers };
}
var mcpServerClientPack = {
kind: "dynamic",
name: "mcp-server-client",
resolve
};
// src/bridge/bridge.ts
var ALL_PACKS = [c2paPack, mcpServerClientPack];
var DEFAULT_ACTIVE_PACKS = [];
function resolveOwnScript() {
const candidates = Array.from(
document.querySelectorAll("script[src]")
);
try {
const selfUrl = import.meta.url;
const exact = candidates.find((s) => s.src === selfUrl);
if (exact) return exact;
} catch {
}
return candidates.find((s) => s.src.includes("/.webmcp/bridge.js")) ?? null;
}
var script = resolveOwnScript();
initBridge().catch((err) => {
console.error("[webmcp-interceptor] Bridge initialisation failed:", err);
});
async function initBridge() {
const mc = resolveModelContext();
if (!mc) {
console.warn(
"[webmcp-interceptor] document.modelContext is not available.\nTo enable WebMCP, use Chrome M146+ and enable:\nchrome://flags/#enable-experimental-web-platform-features"
);
return;
}
const workerBaseUrl = resolveWorkerBaseUrl();
const mcpUrl = normalizeMcpUrl(script?.dataset["mcpUrl"]);
const ctx = {
origin: window.location.origin,
workerRpc: workerBaseUrl ? makeWorkerRpc(workerBaseUrl) : workerRpcUnavailable,
...mcpUrl ? { mcpUrl } : {}
};
const activePacks = selectActivePacks(
ALL_PACKS,
parsePackList(script?.dataset["packs"]),
DEFAULT_ACTIVE_PACKS
);
const staticResolved = resolveStaticPacks(activePacks);
const staticRegistry = buildRegistry(staticResolved);
registerTools(mc, ctx, staticRegistry);
const registeredNames = new Set(staticRegistry.tools.map((t) => t.name));
console.debug(
`[webmcp-interceptor] Registered ${staticRegistry.tools.length} static tool(s); resolving dynamic packs\u2026`
);
const dynamicResolved = await resolveDynamicPacks(activePacks, ctx);
if (dynamicResolved.length > 0) {
const combined = buildRegistry([...staticResolved, ...dynamicResolved]);
const additions = {
tools: combined.tools.filter((t) => !registeredNames.has(t.name)),
handlers: combined.handlers
};
registerTools(mc, ctx, additions);
console.debug(
`[webmcp-interceptor] Registered ${additions.tools.length} dynamic tool(s) from ${dynamicResolved.length} dynamic pack(s).`
);
}
}
function registerTools(mc, ctx, registry) {
for (const tool of registry.tools) {
const handler = registry.handlers[tool.name];
if (!handler) continue;
const webmcpTool = {
name: tool.name,
// MCP's `Tool.description` is optional but WebMCP's
// `registerTool()` requires a non-empty string. Fall back to
// the tool name when the description is missing OR empty (`??`
// alone would let an empty string through) so registration never
// fails — the agent at least sees the name.
description: tool.description || tool.name,
inputSchema: tool.inputSchema,
// WebMCP may pass an options bag with an AbortSignal as the
// second argument; forward it so handlers can cancel in-flight
// work. When absent, the per-call context simply has no signal.
// The spec makes `options` required, but older Chrome builds may
// omit it, so keep the optional chain.
execute: (args, options) => invokeHandler(tool.name, handler, args, {
...ctx,
...options?.signal ? { signal: options.signal } : {}
})
};
const warn = (err) => console.warn(
`[webmcp-interceptor] Failed to register tool "${tool.name}":`,
err
);
try {
Promise.resolve(mc.registerTool(webmcpTool)).catch(warn);
} catch (err) {
warn(err);
}
}
}
async function invokeHandler(toolName, handler, args, ctx) {
let result;
try {
result = await handler(args, ctx);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.warn(`[webmcp-interceptor] Tool "${toolName}" threw:`, message);
throw new Error(message);
}
if (result.isError) {
const text = extractText(result) ?? `${toolName} failed`;
const error = new Error(text, { cause: result });
error.webmcpResult = result;
throw error;
}
if (result.structuredContent !== void 0) return result.structuredContent;
return extractText(result) ?? null;
}
function extractText(r) {
if (!r.content || r.content.length === 0) return null;
const texts = [];
for (const block of r.content) {
if (block.type === "text") texts.push(block.text);
}
return texts.length === 0 ? null : texts.join("\n");
}
function resolveWorkerBaseUrl() {
let src = script?.src;
if (!src) {
try {
src = import.meta.url;
} catch {
src = void 0;
}
}
if (!src) return null;
try {
const url = new URL(src);
const webmcpIndex = url.pathname.indexOf("/.webmcp/");
if (webmcpIndex >= 0) {
url.pathname = url.pathname.slice(0, webmcpIndex);
}
url.search = "";
url.hash = "";
return url.toString().replace(/\/$/, "");
} catch {
return null;
}
}
function makeWorkerRpc(baseUrl) {
return async (path, body, validate) => {
const url = `${baseUrl}/.webmcp/rpc/${path.replace(/^\//, "")}`;
const res = await fetch(url, {
method: "POST",
headers: { "content-type": "application/json" },
// Important: NOT `credentials: 'include'`. The worker is on a
// different origin and we don't want to forward the page's
// cookies to it. Each pack is responsible for sending whatever
// worker-specific auth it needs (today, none).
body: JSON.stringify(body)
});
const text = await res.text();
let parsed;
try {
parsed = text === "" ? null : JSON.parse(text);
} catch {
throw new Error(`Worker returned non-JSON response (HTTP ${res.status})`);
}
if (!res.ok) {
const message = parsed && typeof parsed === "object" && "error" in parsed && typeof parsed.error === "string" ? parsed.error : `Worker RPC failed (HTTP ${res.status})`;
throw new Error(message);
}
return validate(parsed);
};
}
function workerRpcUnavailable() {
throw new Error(
"Worker base URL could not be determined. Worker-backed tools are unavailable on this page."
);
}
function resolveModelContext() {
if (document.modelContext) return document.modelContext;
const fromNavigator = navigator.modelContext;
return fromNavigator ?? null;
}