1// src/tool-pack.ts 2function isDynamicPack(pack) { 3 return pack.kind === "dynamic"; 4} 5function ok(data) { 6 return { 7 content: [{ type: "text", text: JSON.stringify(data) }], 8 structuredContent: data 9 }; 10} 11function customResult(result) { 12 return result; 13} 14function errorResult(message) { 15 return { 16 content: [{ type: "text", text: message }], 17 isError: true 18 }; 19} 20
21// src/bridge/registry.ts 22function parsePackList(raw) { 23 if (raw === void 0) return null; 24 return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0); 25} 26var DEFAULT_MCP_URL = "/mcp"; 27function normalizeMcpUrl(raw) { 28 const v = (raw ?? "").trim(); 29 if (v.toLowerCase() === "none") return void 0; 30 if (v === "") { 31 console.warn( 32 `[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.` 33 ); 34 return DEFAULT_MCP_URL; 35 } 36 return v; 37} 38function selectActivePacks(all, requested, defaults) { 39 const names = requested ?? defaults; 40 const byName = new Map(all.map((p) => [p.name, p])); 41 const active = []; 42 for (const name of names) { 43 const pack = byName.get(name); 44 if (pack) active.push(pack); 45 else 46 console.warn( 47 `[webmcp-interceptor] Requested pack "${name}" is not bundled; skipping.` 48 ); 49 } 50 return active; 51} 52function resolveStaticPacks(packs) { 53 const resolved = []; 54 for (const pack of packs) { 55 if (!isDynamicPack(pack)) { 56 resolved.push({ 57 name: pack.name, 58 tools: pack.tools, 59 handlers: pack.handlers, 60 dynamic: false 61 }); 62 } 63 } 64 return resolved; 65} 66async function resolveDynamicPacks(packs, ctx) { 67 const dynamics = packs.filter(isDynamicPack); 68 return Promise.all( 69 dynamics.map(async (pack) => { 70 try { 71 const { tools, handlers } = await pack.resolve(ctx); 72 return { name: pack.name, tools, handlers, dynamic: true }; 73 } catch (err) { 74 console.warn( 75 `[webmcp-interceptor] Dynamic pack "${pack.name}" failed to resolve; skipping.`, 76 err 77 ); 78 return { name: pack.name, tools: [], handlers: {}, dynamic: true }; 79 } 80 }) 81 ); 82} 83var MAX_TOTAL_TOOLS = 200; 84function buildRegistry(packs) { 85 const tools = []; 86 const handlers = {}; 87 const seen = /* @__PURE__ */ new Map(); 88 for (const pack of packs) { 89 const toolNames = new Set(pack.tools.map((t) => t.name)); 90 for (const tool of pack.tools) { 91 if (!pack.handlers[tool.name]) { 92 throw new Error( 93 `Tool-pack "${pack.name}" declares tool "${tool.name}" but has no matching handler.` 94 ); 95 } 96 } 97 for (const name of Object.keys(pack.handlers)) { 98 if (!toolNames.has(name)) { 99 throw new Error( 100 `Tool-pack "${pack.name}" has a handler for "${name}" with no matching tool descriptor.` 101 ); 102 } 103 } 104 for (const tool of pack.tools) { 105 const existing = seen.get(tool.name); 106 if (existing) { 107 if (pack.dynamic) { 108 console.warn( 109 `[webmcp-interceptor] Tool "${tool.name}" from dynamic pack "${pack.name}" collides with "${existing}"; skipping the dynamic one.` 110 ); 111 continue; 112 } 113 throw new Error( 114 `Tool name collision: "${tool.name}" registered by both "${existing}" and "${pack.name}".` 115 ); 116 } 117 if (tools.length >= MAX_TOTAL_TOOLS) { 118 console.warn( 119 `[webmcp-interceptor] Reached ${MAX_TOTAL_TOOLS}-tool cap; skipping remaining tools from "${pack.name}".` 120 ); 121 return { tools, handlers }; 122 } 123 const handler = pack.handlers[tool.name]; 124 if (!handler) { 125 throw new Error( 126 `Tool-pack "${pack.name}" declares tool "${tool.name}" but has no matching handler.` 127 ); 128 } 129 seen.set(tool.name, pack.name); 130 tools.push(tool); 131 handlers[tool.name] = handler; 132 } 133 } 134 return { tools, handlers }; 135} 136
137// src/bridge/packs/c2pa/cbor.ts 138var BREAK = 255; 139var textDecoder = new TextDecoder("utf-8", { fatal: false }); 140var MAX_ITEMS = 1e6; 141var MAX_INDEFINITE_BYTES = 16 * 1024 * 1024; 142function decodeCbor(bytes) { 143 return new CborDecoder(bytes).decode(); 144} 145function isCborTag(v) { 146 return typeof v === "object" && v !== null && !(v instanceof Uint8Array) && !Array.isArray(v) && !(v instanceof Map) && "tag" in v; 147} 148function isCborSimple(v) { 149 return typeof v === "object" && v !== null && !(v instanceof Uint8Array) && !Array.isArray(v) && !(v instanceof Map) && "simple" in v; 150} 151var CborDecoder = class { 152 constructor(bytes) { 153 this.bytes = bytes; 154 this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); 155 } 156 bytes; 157 view; 158 off = 0; 159 itemCount = 0; 160 decode() { 161 return this.readValue(0); 162 } 163 /** Read one item. `depth` guards against pathological nesting. */ 164 readValue(depth) { 165 if (depth > 256) throw new Error("CBOR nesting too deep"); 166 if (++this.itemCount > MAX_ITEMS) throw new Error("CBOR item count exceeds budget"); 167 const ib = this.u8(); 168 const major = ib >> 5; 169 const ai = ib & 31; 170 switch (major) { 171 case 0: 172 return this.readUint(ai); 173 case 1: { 174 const n = this.readUint(ai); 175 return typeof n === "bigint" ? -1n - n : -1 - n; 176 } 177 case 2: 178 return this.readByteString(ai, depth, 2); 179 case 3: 180 return textDecoder.decode(this.readByteString(ai, depth, 3)); 181 case 4: 182 return this.readArray(ai, depth); 183 case 5: 184 return this.readMap(ai, depth); 185 case 6: { 186 const tag = this.asLength(this.readUint(ai)); 187 return { tag, value: this.readValue(depth + 1) }; 188 } 189 default: 190 return this.readSimple(ai); 191 } 192 } 193 /** Read the argument that follows the initial byte (major 0/1/6). */ 194 readUint(ai) { 195 if (ai < 24) return ai; 196 if (ai === 24) return this.u8(); 197 if (ai === 25) { 198 const v = this.view.getUint16(this.off); 199 this.off += 2; 200 return v; 201 } 202 if (ai === 26) { 203 const v = this.view.getUint32(this.off); 204 this.off += 4; 205 return v; 206 } 207 if (ai === 27) { 208 const v = this.view.getBigUint64(this.off); 209 this.off += 8; 210 return v <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(v) : v; 211 } 212 throw new Error(`Invalid CBOR additional-info value: ${ai}`); 213 } 214 readByteString(ai, depth, majorType) { 215 if (ai === 31) { 216 const chunks = []; 217 let total = 0; 218 for (; ; ) { 219 const ib = this.peek(); 220 if (ib === BREAK) { 221 this.off += 1; 222 break; 223 } 224 const major = ib >> 5; 225 const chunkAi = ib & 31; 226 if (major !== majorType || chunkAi === 31) { 227 throw new Error("Indefinite-length string chunk must be a definite-length string of the same type"); 228 } 229 this.off += 1; 230 if (++this.itemCount > MAX_ITEMS) throw new Error("CBOR item count exceeds budget"); 231 const chunk = this.readByteString(chunkAi, depth + 1, majorType); 232 chunks.push(chunk); 233 total += chunk.length; 234 if (total > MAX_INDEFINITE_BYTES) { 235 throw new Error("Indefinite-length string exceeds size limit"); 236 } 237 } 238 const out2 = new Uint8Array(total); 239 let p = 0; 240 for (const c of chunks) { 241 out2.set(c, p); 242 p += c.length; 243 } 244 return out2; 245 } 246 const len = this.asLength(this.readUint(ai)); 247 const end = this.off + len; 248 if (end > this.bytes.length) throw new Error("CBOR byte string truncated"); 249 const out = this.bytes.subarray(this.off, end); 250 this.off = end; 251 return out; 252 } 253 readArray(ai, depth) { 254 const out = []; 255 if (ai === 31) { 256 while (this.peek() !== BREAK) out.push(this.readValue(depth + 1)); 257 this.off += 1; 258 return out; 259 } 260 const len = this.asLength(this.readUint(ai)); 261 for (let i = 0; i < len; i++) out.push(this.readValue(depth + 1)); 262 return out; 263 } 264 readMap(ai, depth) { 265 const out = /* @__PURE__ */ new Map(); 266 if (ai === 31) { 267 while (this.peek() !== BREAK) {
268 const k = this.readValue(depth + 1); 269 out.set(k, this.readValue(depth + 1)); 270 } 271 this.off += 1; 272 return out; 273 } 274 const len = this.asLength(this.readUint(ai)); 275 for (let i = 0; i < len; i++) { 276 const k = this.readValue(depth + 1); 277 out.set(k, this.readValue(depth + 1)); 278 } 279 return out; 280 } 281 readSimple(ai) { 282 switch (ai) { 283 case 20: 284 return false; 285 case 21: 286 return true; 287 case 22: 288 return null; 289 case 23: 290 return void 0; 291 case 24: 292 return { simple: this.u8() }; 293 // one-byte simple value (32-255) 294 case 25: 295 return this.readHalfFloat(); 296 case 26: { 297 const v = this.view.getFloat32(this.off); 298 this.off += 4; 299 return v; 300 } 301 case 27: { 302 const v = this.view.getFloat64(this.off); 303 this.off += 8; 304 return v; 305 } 306 default: 307 return { simple: ai }; 308 } 309 } 310 /** IEEE 754 half-precision (binary16) â JS number. */ 311 readHalfFloat() { 312 const half = this.view.getUint16(this.off); 313 this.off += 2; 314 const exp = (half & 31744) >> 10; 315 const frac = half & 1023; 316 const sign = half & 32768 ? -1 : 1; 317 if (exp === 0) return sign * 2 ** -14 * (frac / 1024); 318 if (exp === 31) return frac ? NaN : sign * Infinity; 319 return sign * 2 ** (exp - 15) * (1 + frac / 1024); 320 } 321 u8() { 322 if (this.off >= this.bytes.length) throw new Error("CBOR truncated"); 323 const v = this.view.getUint8(this.off); 324 this.off += 1; 325 return v; 326 } 327 peek() { 328 if (this.off >= this.bytes.length) throw new Error("CBOR truncated"); 329 return this.view.getUint8(this.off); 330 } 331 /** Coerce a length/tag argument to a sane JS number or throw. */ 332 asLength(v) { 333 if (typeof v === "bigint" || v > 2147483647) { 334 throw new Error("CBOR length/tag exceeds supported range"); 335 } 336 return v; 337 } 338}; 339
340// src/bridge/packs/c2pa/x509.ts 341var textDecoder2 = new TextDecoder("utf-8", { fatal: false }); 342var OID_COMMON_NAME = [85, 4, 3]; 343function parseCertificate(der) { 344 try { 345 return parseCertificateUnsafe(der); 346 } catch { 347 return {}; 348 } 349} 350function parseCertificateUnsafe(der) { 351 const view = new DataView(der.buffer, der.byteOffset, der.byteLength); 352 const cert = readTlv(view, 0, der.length); 353 if (!cert) return {}; 354 const tbs = readTlv(view, cert.contentStart, cert.contentEnd); 355 if (!tbs) return {}; 356 const fields = readChildren(view, der, tbs.contentStart, tbs.contentEnd); 357 let i = 0; 358 if (fields[i] && fields[i]?.tag === 160) i += 1; 359 const issuer = fields[i + 2]; 360 const validity = fields[i + 3]; 361 const subject = fields[i + 4]; 362 const info = {}; 363 if (issuer) info.issuerCN = extractCommonName(view, der, issuer); 364 if (subject) info.subjectCN = extractCommonName(view, der, subject); 365 if (validity) { 366 const times = readChildren(view, der, validity.contentStart, validity.contentEnd); 367 if (times[0]) info.notBefore = parseTime(der, times[0]); 368 if (times[1]) info.notAfter = parseTime(der, times[1]); 369 } 370 return info; 371} 372function readTlv(view, off, limit) { 373 if (off + 2 > limit) return null; 374 const tag = view.getUint8(off); 375 let p = off + 1; 376 let len = view.getUint8(p); 377 p += 1; 378 if (len & 128) { 379 const n = len & 127; 380 if (n === 0 || n > 4) return null; 381 len = 0; 382 for (let i = 0; i < n; i++) { 383 len = len << 8 | view.getUint8(p); 384 p += 1; 385 } 386 } 387 const contentStart = p; 388 const contentEnd = p + len; 389 if (contentEnd > limit) return null; 390 return { tag, contentStart, contentEnd, end: contentEnd }; 391} 392function readChildren(view, bytes, start, end) { 393 const out = []; 394 let off = start; 395 while (off < end) { 396 const node = readTlv(view, off, end); 397 if (!node) break; 398 out.push(node); 399 off = node.end; 400 } 401 return out; 402} 403function extractCommonName(view, bytes, name) { 404 for (const rdn of readChildren(view, bytes, name.contentStart, name.contentEnd)) { 405 for (const atv of readChildren(view, bytes, rdn.contentStart, rdn.contentEnd)) { 406 const parts = readChildren(view, bytes, atv.contentStart, atv.contentEnd); 407 const oid = parts[0]; 408 const value = parts[1]; 409 if (oid && value && oid.tag === 6 && oidEquals(bytes, oid, OID_COMMON_NAME)) { 410 return textDecoder2.decode(bytes.subarray(value.contentStart, value.contentEnd)); 411 } 412 } 413 } 414 return void 0; 415} 416function oidEquals(bytes, node, expected) { 417 const len = node.contentEnd - node.contentStart; 418 if (len !== expected.length) return false; 419 for (let i = 0; i < len; i++) { 420 if (bytes[node.contentStart + i] !== expected[i]) return false; 421 } 422 return true; 423} 424function parseTime(bytes, node) { 425 const raw = textDecoder2.decode(bytes.subarray(node.contentStart, node.contentEnd)); 426 const isUtc = node.tag === 23; 427 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); 428 if (!m) return raw; 429 let year = Number(m[1]); 430 if (isUtc) year += year < 50 ? 2e3 : 1900; 431 const iso = `${pad(year, 4)}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6] ?? "00"}Z`; 432 return iso; 433} 434function pad(n, width) { 435 return String(n).padStart(width, "0"); 436} 437
438// src/bridge/packs/c2pa/manifest.ts 439function buildManifestStore(store) { 440 const manifests = store.children.map(decodeManifest); 441 const active = manifests[manifests.length - 1]; 442 const result = { 443 manifestCount: manifests.length, 444 manifests 445 }; 446 if (store.label !== void 0) result.label = store.label; 447 if (active?.label !== void 0) result.activeManifestLabel = active.label; 448 return result; 449} 450function decodeManifest(node) { 451 let claimNode; 452 let signatureNode; 453 let assertionStore; 454 for (const child of node.children) { 455 const label = child.label ?? ""; 456 if (label.startsWith("c2pa.claim") || child.contentType === "c2cl") { 457 claimNode = child; 458 } else if (label === "c2pa.signature" || child.contentType === "c2cs") { 459 signatureNode = child; 460 } else if (label === "c2pa.assertions" || child.contentType === "c2as") { 461 assertionStore = child; 462 } 463 } 464 const assertions = assertionStore ? assertionStore.children.map(decodeAssertion) : []; 465 const manifest = { assertions }; 466 if (node.label !== void 0) manifest.label = node.label; 467 if (claimNode?.cbor !== void 0) { 468 const claim = cborToJson(claimNode.cbor); 469 if (claim !== null && typeof claim === "object" && !Array.isArray(claim)) { 470 manifest.claim = claim; 471 } 472 } 473 if (signatureNode?.cbor !== void 0) { 474 manifest.signature = decodeCoseSignature(signatureNode.cbor); 475 } 476 return manifest; 477} 478function decodeAssertion(node) { 479 const out = {}; 480 if (node.label !== void 0) out.label = node.label; 481 if (node.cbor !== void 0) { 482 out.data = cborToJson(node.cbor); 483 } else if (node.json !== void 0) { 484 out.data = node.json; 485 } else if (node.binary !== void 0) { 486 out.binary = node.binary; 487 } 488 return out; 489} 490var COSE_ALG = { 491 [-7]: "ES256", 492 [-35]: "ES384", 493 [-36]: "ES512", 494 [-8]: "EdDSA", 495 [-37]: "PS256", 496 [-38]: "PS384", 497 [-39]: "PS512", 498 [-257]: "RS256", 499 [-258]: "RS384", 500 [-259]: "RS512" 501}; 502function decodeCoseSignature(cose) { 503 const arr = isCborTag(cose) ? cose.value : cose; 504 if (!Array.isArray(arr) || arr.length < 4) return {}; 505 const protectedHeader = decodeProtectedHeader(arr[0]); 506 const unprotected = arr[1] instanceof Map ? arr[1] : /* @__PURE__ */ new Map(); 507 const sig = {}; 508 const algId = asInt(protectedHeader.get(1) ?? unprotected.get(1)); 509 if (algId !== void 0) sig.alg = COSE_ALG[algId] ?? `COSE(${algId})`; 510 const certs = readX5Chain(protectedHeader.get(33) ?? unprotected.get(33)); 511 if (certs.length > 0) { 512 sig.certChainLength = certs.length; 513 const first = certs[0]; 514 if (first) { 515 const info = parseCertificate(first); 516 if (info.subjectCN !== void 0) sig.signedBy = info.subjectCN; 517 if (info.issuerCN !== void 0) sig.certifiedBy = info.issuerCN; 518 if (info.notBefore !== void 0) sig.certValidFrom = info.notBefore; 519 if (info.notAfter !== void 0) sig.certValidTo = info.notAfter; 520 } 521 } 522 return sig; 523} 524function decodeProtectedHeader(value) { 525 if (value instanceof Uint8Array && value.length > 0) { 526 const decoded = decodeCbor(value); 527 if (decoded instanceof Map) return decoded; 528 } 529 return /* @__PURE__ */ new Map(); 530} 531function readX5Chain(value) { 532 if (value instanceof Uint8Array) return [value]; 533 if (Array.isArray(value)) { 534 return value.filter((c) => c instanceof Uint8Array); 535 } 536 return []; 537} 538function asInt(value) { 539 if (typeof value === "number" && Number.isInteger(value)) return value; 540 if (typeof value === "bigint") return Number(value); 541 return void 0; 542} 543var MAX_INLINE_BYTES = 64; 544function cborToJson(value) { 545 if (value === null || value === void 0) return value ?? null; 546 if (typeof value === "bigint") return value.toString(); 547 if (value instanceof Uint8Array) return bytesToJson(value); 548 if (Array.isArray(value)) return value.map(cborToJson); 549 if (value instanceof Map) { 550 const obj = /* @__PURE__ */ Object.create(null); 551 for (const [k, v] of value) obj[keyToString(k)] = cborToJson(v); 552 return obj; 553 } 554 if (isCborTag(value)) { 555 return { $tag: value.tag, value: cborToJson(value.value) }; 556 } 557 if (isCborSimple(value)) { 558 return { $simple: value.simple }; 559 } 560 return value; 561} 562function bytesToJson(b) { 563 if (b.length <= MAX_INLINE_BYTES) return { $bytes: b.length, hex: toHex(b) }; 564 return { $bytes: b.length, hex: `${toHex(b.subarray(0, 32))}\u2026` }; 565} 566function keyToString(k) { 567 if (typeof k === "string") return k; 568 if (typeof k === "number" || typeof k === "bigint" || typeof k === "boolean") { 569 return String(k); 570 } 571 return JSON.stringify(cborToJson(k)); 572} 573function toHex(b) { 574 let s = ""; 575 for (let i = 0; i < b.length; i++) { 576 const byte = b[i]; 577 if (byte === void 0) break; 578 s += byte.toString(16).padStart(2, "0"); 579 } 580 return s; 581} 582
583// src/bridge/packs/c2pa/jumbf.ts 584var textDecoder3 = new TextDecoder("utf-8", { fatal: false }); 585var MAX_JUMBF_BOXES = 1e4; 586var MAX_APP11_PACKETS = 1024; 587var MAX_FRAGMENTS_PER_INSTANCE = 256; 588function detectImageFormat(bytes) { 589 if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) { 590 return "jpeg"; 591 } 592 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) { 593 return "png"; 594 } 595 return "unknown"; 596} 597function extractC2paJumbf(bytes) { 598 switch (detectImageFormat(bytes)) { 599 case "jpeg": 600 return extractFromJpeg(bytes); 601 case "png": 602 return extractFromPng(bytes); 603 default: 604 return null; 605 } 606} 607function parseManifestStore(jumbf) { 608 const view = new DataView(jumbf.buffer, jumbf.byteOffset, jumbf.byteLength); 609 const top = readBox(view, jumbf, 0, jumbf.length); 610 if (!top || top.type !== "jumb") return null; 611 return parseSuperbox(view, jumbf, top.payloadStart, top.payloadEnd, 0, { 612 count: 0 613 }); 614} 615function extractFromJpeg(bytes) { 616 const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); 617 if (view.getUint16(0) !== 65496) return null; 618 const packets = /* @__PURE__ */ new Map(); 619 let app11Count = 0; 620 let off = 2; 621 while (off + 2 <= bytes.length) {
622 if (view.getUint8(off) !== 255) break; 623 let marker = view.getUint8(off + 1); 624 while (marker === 255 && off + 2 < bytes.length) { 625 off += 1; 626 marker = view.getUint8(off + 1); 627 } 628 off += 2; 629 if (marker === 217 || marker === 218) break; 630 if (marker === 1 || marker >= 208 && marker <= 215) continue; 631 if (off + 2 > bytes.length) break; 632 const segLen = view.getUint16(off); 633 const segStart = off + 2; 634 const segEnd = off + segLen; 635 if (segLen < 2 || segEnd > bytes.length) break; 636 if (marker === 235 && segEnd - segStart >= 8) { 637 if (view.getUint16(segStart) === 19024) { 638 if (++app11Count > MAX_APP11_PACKETS) break; 639 const en = view.getUint16(segStart + 2); 640 const z = view.getUint32(segStart + 4); 641 const data = bytes.subarray(segStart + 8, segEnd); 642 const arr = packets.get(en) ?? []; 643 if (arr.length < MAX_FRAGMENTS_PER_INSTANCE) { 644 arr.push({ z, data }); 645 packets.set(en, arr); 646 } 647 } 648 } 649 off = segEnd; 650 } 651 for (const arr of packets.values()) { 652 arr.sort((a, b) => a.z - b.z); 653 const first = arr[0]; 654 if (!first) continue; 655 let headerLen = 8; 656 if (first.data.length >= 4) { 657 const fv = new DataView( 658 first.data.buffer, 659 first.data.byteOffset, 660 first.data.byteLength 661 ); 662 if (fv.getUint32(0) === 1) headerLen = 16; 663 } 664 const pieces = []; 665 let total = 0; 666 for (let i = 0; i < arr.length; i++) { 667 const pk = arr[i]; 668 if (!pk) continue; 669 const piece = i === 0 ? pk.data : pk.data.subarray(headerLen); 670 pieces.push(piece); 671 total += piece.length; 672 } 673 const buf = new Uint8Array(total); 674 let p = 0; 675 for (const piece of pieces) { 676 buf.set(piece, p); 677 p += piece.length; 678 } 679 if (isC2paStore(buf)) return buf; 680 } 681 return null; 682} 683function extractFromPng(bytes) { 684 const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); 685 let off = 8; 686 while (off + 8 <= bytes.length) { 687 const len = view.getUint32(off); 688 const type = ascii(bytes, off + 4, 4); 689 const dataStart = off + 8; 690 const dataEnd = dataStart + len; 691 if (dataEnd + 4 > bytes.length) break; 692 if (type === "caBX") { 693 const buf = bytes.subarray(dataStart, dataEnd); 694 return isC2paStore(buf) ? buf : null; 695 } 696 if (type === "IEND") break; 697 off = dataEnd + 4; 698 } 699 return null; 700} 701function readBox(view, bytes, offset, limit) { 702 if (offset + 8 > limit) return null; 703 let len = view.getUint32(offset); 704 const type = ascii(bytes, offset + 4, 4); 705 let headerLen = 8; 706 if (len === 1) { 707 if (offset + 16 > limit) return null; 708 const hi = view.getUint32(offset + 8); 709 const lo = view.getUint32(offset + 12); 710 len = hi * 4294967296 + lo; 711 headerLen = 16; 712 } else if (len === 0) { 713 len = limit - offset; 714 } 715 const boxEnd = offset + len; 716 if (len < headerLen || boxEnd > limit) return null; 717 return { type, payloadStart: offset + headerLen, payloadEnd: boxEnd, boxEnd }; 718} 719function parseSuperbox(view, bytes, start, end, depth, counter) { 720 const node = { contentType: "", children: [] }; 721 if (depth > 64) return node; 722 let off = start; 723 const first = readBox(view, bytes, off, end); 724 if (first && first.type === "jumd") { 725 if (++counter.count > MAX_JUMBF_BOXES) return node; 726 parseDescription(view, bytes, first.payloadStart, first.payloadEnd, node); 727 off = first.boxEnd; 728 } 729 while (off < end) { 730 if (++counter.count > MAX_JUMBF_BOXES) return node; 731 const box = readBox(view, bytes, off, end); 732 if (!box) break; 733 switch (box.type) { 734 case "jumb": 735 node.children.push( 736 parseSuperbox(view, bytes, box.payloadStart, box.payloadEnd, depth + 1, counter) 737 ); 738 break; 739 case "cbor": 740 try { 741 node.cbor = decodeCbor(bytes.subarray(box.payloadStart, box.payloadEnd)); 742 } catch { 743 } 744 break; 745 case "json": 746 try { 747 node.json = JSON.parse( 748 textDecoder3.decode(bytes.subarray(box.payloadStart, box.payloadEnd)) 749 ); 750 } catch { 751 } 752 break; 753 case "bidb": 754 // embedded-file data box 755 case "uuid": 756 node.binary = { size: box.payloadEnd - box.payloadStart }; 757 break; 758 default: 759 break; 760 } 761 off = box.boxEnd; 762 } 763 return node; 764} 765function parseDescription(view, bytes, start, end, node) { 766 if (start + 17 > end) return; 767 node.contentType = ascii(bytes, start, 4); 768 const toggles = view.getUint8(start + 16); 769 let off = start + 17; 770 if (toggles & 2) { 771 let z = off; 772 while (z < end && view.getUint8(z) !== 0) z += 1; 773 node.label = textDecoder3.decode(bytes.subarray(off, z)); 774 } 775} 776function isC2paStore(buf) { 777 if (buf.length < 16) return false;
778 const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); 779 const top = readBox(view, buf, 0, buf.length); 780 if (!top || top.type !== "jumb") return false; 781 const desc = readBox(view, buf, top.payloadStart, top.payloadEnd); 782 if (!desc || desc.type !== "jumd") return false; 783 return ascii(buf, desc.payloadStart, 4) === "c2pa"; 784} 785function ascii(bytes, off, len) { 786 let s = ""; 787 for (let i = 0; i < len; i++) { 788 const c = bytes[off + i]; 789 if (c === void 0) break; 790 s += String.fromCharCode(c); 791 } 792 return s; 793} 794
795// src/bridge/packs/c2pa/c2pa-executor.ts 796var MAX_SCAN_IMAGES = 100; 797var MAX_IMAGE_BYTES = 64 * 1024 * 1024; 798var FETCH_TIMEOUT_MS = 15e3; 799var SCAN_CONCURRENCY = 6; 800var SCAN_BUDGET_MS = 3e4; 801var NO_VERIFICATION_NOTE = "Manifest decoded only. Cryptographic signature, certificate-chain, and trust-list verification were NOT performed; treat provenance as unverified claims."; 802async function scanImages(limit, signal) { 803 const allSources = collectImageSources(); 804 const capped = Math.min(limit, MAX_SCAN_IMAGES); 805 const sources = allSources.slice(0, capped); 806 const deadline = anySignal(signal, AbortSignal.timeout(SCAN_BUDGET_MS)); 807 const results = await mapWithConcurrency( 808 sources, 809 SCAN_CONCURRENCY, 810 (src) => scanOne(src, deadline) 811 ); 812 const withC2pa = results.filter((r) => r.hasC2pa).length; 813 return { 814 imageCount: allSources.length, 815 scanned: results.length, 816 withC2pa, 817 results 818 }; 819} 820async function scanOne(src, signal) { 821 try { 822 const bytes = await fetchImageBytes(src, signal); 823 const analysis = analyzeBytes(bytes); 824 if (!analysis.store) { 825 return { src, hasC2pa: false, format: analysis.format }; 826 } 827 const active = activeManifest(analysis.store); 828 const summary = { 829 src, 830 hasC2pa: true, 831 format: analysis.format, 832 manifestCount: analysis.store.manifestCount, 833 signatureVerified: false 834 }; 835 const claimGenerator = readClaimGenerator(active?.claim); 836 const title = readTitle(active?.claim); 837 if (claimGenerator !== void 0) summary.claimGenerator = claimGenerator; 838 if (title !== void 0) summary.title = title; 839 if (active?.signature?.signedBy !== void 0) { 840 summary.signedBy = active.signature.signedBy; 841 } 842 return summary; 843 } catch (err) { 844 return { src, hasC2pa: false, error: errorMessage(err) }; 845 } 846} 847async function inspectImage(opts, signal) { 848 const isUrlMode = Boolean(opts.url); 849 const src = resolveSource(opts); 850 const bytes = await fetchImageBytes(src, signal, isUrlMode); 851 const analysis = analyzeBytes(bytes); 852 if (!analysis.store) { 853 return { 854 src, 855 hasC2pa: false, 856 format: analysis.format, 857 message: analysis.format === "unknown" ? "Unsupported image format (only JPEG and PNG are parsed today)." : "No C2PA manifest found in this image." 858 }; 859 } 860 return { 861 src, 862 hasC2pa: true, 863 format: analysis.format, 864 manifestStore: analysis.store, 865 validation: { signatureVerified: false, note: NO_VERIFICATION_NOTE } 866 }; 867} 868function analyzeBytes(bytes) { 869 const format = detectImageFormat(bytes); 870 const jumbf = extractC2paJumbf(bytes); 871 if (!jumbf) return { format }; 872 const tree = parseManifestStore(jumbf); 873 if (!tree) return { format }; 874 return { format, store: buildManifestStore(tree) }; 875} 876function collectImageSources() { 877 const seen = /* @__PURE__ */ new Set(); 878 const out = []; 879 for (const img of Array.from(document.querySelectorAll("img"))) { 880 const el = img; 881 const src = el.currentSrc || el.src; 882 if (src && !seen.has(src)) { 883 seen.add(src); 884 out.push(src); 885 } 886 } 887 return out; 888} 889function resolveSource(opts) { 890 if (opts.url) { 891 const resolved = new URL(opts.url, document.location.href); 892 if (resolved.origin !== document.location.origin) { 893 throw new Error("URL must be same-origin."); 894 } 895 return resolved.toString(); 896 } 897 if (opts.selector) { 898 const el = document.querySelector(opts.selector); 899 if (!el) throw new Error(`No element matches selector: ${opts.selector}`); 900 if (!(el instanceof HTMLImageElement)) { 901 throw new Error(`Element is not an <img>: ${opts.selector}`); 902 } 903 const src = el.currentSrc || el.src; 904 if (!src) throw new Error(`<img> has no resolved src: ${opts.selector}`); 905 return src; 906 } 907 throw new Error("Provide either a `selector` or a `url`."); 908} 909async function fetchImageBytes(src, signal, noRedirect = false) { 910 const combined = anySignal(signal, AbortSignal.timeout(FETCH_TIMEOUT_MS)); 911 let res; 912 try { 913 res = await fetch(src, { 914 signal: combined, 915 // Only block redirects when the agent supplied the URL. 916 // Scan/selector mode fetches already-resolved currentSrc URLs. 917 ...noRedirect ? { redirect: "error" } : {} 918 }); 919 } catch (err) { 920 if (err instanceof DOMException && err.name === "TimeoutError") { 921 throw new Error("Image fetch timed out"); 922 } 923 if (err instanceof DOMException && err.name === "AbortError") { 924 throw new Error("Image fetch aborted"); 925 } 926 throw err; 927 } 928 if (!res.ok) throw new Error("Image fetch failed."); 929 const length = Number(res.headers.get("content-length") ?? "0"); 930 if (length > MAX_IMAGE_BYTES) { 931 throw new Error(`Image too large to inspect (${length} bytes)`); 932 } 933 if (!res.body) throw new Error("Response has no body stream"); 934 const reader = res.body.getReader(); 935 const chunks = []; 936 let total = 0; 937 for (; ; ) { 938 const { done, value } = await reader.read(); 939 if (done) break; 940 total += value.byteLength; 941 if (total > MAX_IMAGE_BYTES) { 942 await reader.cancel(); 943 throw new Error(`Image too large to inspect (${total} bytes)`); 944 } 945 chunks.push(value); 946 } 947 const assembled = new Uint8Array(total); 948 let offset = 0; 949 for (const chunk of chunks) { 950 assembled.set(chunk, offset); 951 offset += chunk.byteLength; 952 } 953 return assembled; 954} 955function activeManifest(store) { 956 return store.manifests[store.manifests.length - 1]; 957} 958function readClaimGenerator(claim) { 959 if (!claim) return void 0; 960 const info = claim["claim_generator_info"]; 961 if (Array.isArray(info) && info.length > 0) { 962 const first = info[0]; 963 if (first && typeof first === "object") { 964 const name = first["name"]; 965 const version = first["version"]; 966 if (typeof name === "string") { 967 return typeof version === "string" ? `${name} ${version}` : name; 968 } 969 } 970 } 971 const generator = claim["claim_generator"]; 972 return typeof generator === "string" ? generator : void 0; 973} 974function readTitle(claim) { 975 if (!claim) return void 0; 976 const title = claim["dc:title"] ?? claim["title"]; 977 return typeof title === "string" ? title : void 0; 978} 979function errorMessage(err) {
980 return err instanceof Error ? err.message : String(err); 981} 982function anySignal(optional, required) { 983 return optional ? AbortSignal.any([optional, required]) : required; 984} 985async function mapWithConcurrency(items, concurrency, fn) { 986 const results = new Array(items.length); 987 let next = 0; 988 const workers = []; 989 const limit = Math.max(1, Math.min(concurrency, items.length)); 990 for (let w = 0; w < limit; w++) { 991 workers.push( 992 (async () => { 993 for (; ; ) { 994 const i = next++; 995 if (i >= items.length) return; 996 const item = items[i]; 997 if (item === void 0) continue; 998 results[i] = await fn(item); 999 } 1000 })() 1001 ); 1002 } 1003 await Promise.all(workers); 1004 return results; 1005} 1006
1007// src/bridge/packs/c2pa/index.ts 1008var TOOLS = [ 1009 { 1010 name: "scan_images_c2pa", 1011 description: "Scan every <img> 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.", 1012 inputSchema: { 1013 type: "object", 1014 properties: { 1015 limit: { 1016 type: "integer", 1017 minimum: 1, 1018 maximum: 100, 1019 default: 25, 1020 description: "Maximum number of images to fetch and scan (default 25, max 100)." 1021 } 1022 }, 1023 required: [] 1024 } 1025 }, 1026 { 1027 name: "inspect_image_c2pa", 1028 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.", 1029 inputSchema: { 1030 type: "object", 1031 properties: { 1032 selector: { 1033 type: "string", 1034 maxLength: 8192, 1035 description: "CSS selector for an <img> element on the page." 1036 }, 1037 url: { 1038 type: "string", 1039 maxLength: 8192, 1040 description: "Same-origin image URL (absolute same-origin, or relative to the current page) to fetch and inspect. Cross-origin URLs are rejected." 1041 } 1042 }, 1043 required: [] 1044 } 1045 } 1046]; 1047function optionalInt(args, key, min, max, fallback) { 1048 if (typeof args !== "object" || args === null) return fallback; 1049 const v = args[key]; 1050 if (v === void 0) return fallback; 1051 if (typeof v !== "number" || !Number.isInteger(v)) return null; 1052 if (v < min || v > max) return null; 1053 return v; 1054} 1055var MAX_STRING_ARG = 8192; 1056function readStringArg(args, key) { 1057 if (typeof args !== "object" || args === null) return { kind: "absent" }; 1058 const v = args[key]; 1059 if (v === void 0 || v === null) return { kind: "absent" }; 1060 if (typeof v !== "string") { 1061 return { kind: "invalid", reason: `\`${key}\` must be a string.` }; 1062 } 1063 if (v.length === 0) return { kind: "absent" }; 1064 if (v.length > MAX_STRING_ARG) { 1065 return { 1066 kind: "invalid", 1067 reason: `\`${key}\` exceeds the ${MAX_STRING_ARG}-character limit.` 1068 }; 1069 } 1070 return { kind: "ok", value: v }; 1071} 1072var handleScanImages = async (rawArgs, ctx) => { 1073 const limit = optionalInt(rawArgs, "limit", 1, 100, 25); 1074 if (limit === null) return errorResult("Invalid tool arguments"); 1075 return ok({ ...await scanImages(limit, ctx.signal) }); 1076}; 1077var handleInspectImage = async (rawArgs, ctx) => { 1078 const selector = readStringArg(rawArgs, "selector"); 1079 const url = readStringArg(rawArgs, "url"); 1080 if (selector.kind === "invalid") return errorResult(selector.reason); 1081 if (url.kind === "invalid") return errorResult(url.reason); 1082 const hasSelector = selector.kind === "ok"; 1083 const hasUrl = url.kind === "ok"; 1084 if (!hasSelector && !hasUrl) { 1085 return errorResult("Provide either `selector` or `url`."); 1086 } 1087 if (hasSelector && hasUrl) { 1088 return errorResult("Provide only one of `selector` or `url`, not both."); 1089 } 1090 const result = await inspectImage( 1091 { 1092 ...selector.kind === "ok" ? { selector: selector.value } : {}, 1093 ...url.kind === "ok" ? { url: url.value } : {} 1094 }, 1095 ctx.signal 1096 ); 1097 return ok({ ...result }); 1098}; 1099function wrapErrors(h) { 1100 return async (args, ctx) => { 1101 try { 1102 return await h(args, ctx); 1103 } catch (err) { 1104 return errorResult(err instanceof Error ? err.message : String(err)); 1105 } 1106 }; 1107} 1108var c2paPack = { 1109 kind: "static", 1110 name: "c2pa", 1111 tools: TOOLS, 1112 handlers: { 1113 scan_images_c2pa: wrapErrors(handleScanImages), 1114 inspect_image_c2pa: wrapErrors(handleInspectImage) 1115 } 1116}; 1117
1118// src/bridge/packs/mcp-server-client/mcp-http-utils.ts 1119var MCP_TIMEOUT_MS = 2e4; 1120var MCP_MAX_BYTES = 8 * 1024 * 1024; 1121var MCP_MAX_RESPONSE_CHUNKS = 65536; 1122var MCP_MAX_LIST_PAGES = 20; 1123var MCP_MAX_TOOLS = 200; 1124var MCP_MAX_STRUCTURED_CONTENT_BYTES = 64 * 1024; 1125var MCP_MAX_CONTENT_TEXT_BYTES = 64 * 1024; 1126var MCP_MAX_CONTENT_TEXT_TOTAL_BYTES = 256 * 1024; 1127var RPC_ID = 1; 1128var MCP_CLIENT_INFO = { 1129 name: "cloudflare-webmcp-bridge", 1130 version: "0.0.1" 1131}; 1132var encoder = new TextEncoder(); 1133var McpHttpError = class extends Error { 1134 constructor(status, rpcError) { 1135 super(rpcError?.message ?? `MCP endpoint returned HTTP ${status}`); 1136 this.status = status; 1137 this.rpcError = rpcError; 1138 } 1139 status; 1140 rpcError; 1141 _tag = "McpHttpError"; 1142}; 1143function isObject(v) { 1144 return typeof v === "object" && v !== null && !Array.isArray(v); 1145} 1146function sliceUtf8Bytes(s, maxBytes) { 1147 const bytes = encoder.encode(s); 1148 if (bytes.length <= maxBytes) return s; 1149 let end = maxBytes; 1150 while (end > 0) { 1151 const b = bytes[end]; 1152 if (b === void 0 || (b & 192) !== 128) break; 1153 end--; 1154 } 1155 return new TextDecoder("utf-8", { fatal: false }).decode( 1156 bytes.slice(0, end) 1157 ); 1158} 1159async function readBoundedText(res) { 1160 const body = res.body; 1161 if (!body) return res.text(); 1162 const reader = body.getReader(); 1163 const chunks = []; 1164 let total = 0; 1165 let done = false; 1166 try { 1167 for (let count = 0; count < MCP_MAX_RESPONSE_CHUNKS; count++) { 1168 const { done: streamDone, value } = await reader.read(); 1169 if (streamDone) { 1170 done = true; 1171 break; 1172 } 1173 if (value) { 1174 total += value.byteLength; 1175 if (total > MCP_MAX_BYTES) { 1176 throw new Error( 1177 `MCP response exceeded ${MCP_MAX_BYTES} bytes; aborting.` 1178 ); 1179 } 1180 chunks.push(value); 1181 } 1182 } 1183 if (!done) throw new Error("MCP response exceeded chunk limit; aborting."); 1184 } finally { 1185 await reader.cancel().catch(() => { 1186 }); 1187 } 1188 const merged = new Uint8Array(total); 1189 let offset = 0; 1190 for (const chunk of chunks) { 1191 merged.set(chunk, offset); 1192 offset += chunk.byteLength; 1193 } 1194 return new TextDecoder().decode(merged); 1195} 1196function callSignal(signal) { 1197 const timeout = AbortSignal.timeout(MCP_TIMEOUT_MS); 1198 return signal ? AbortSignal.any([signal, timeout]) : timeout; 1199} 1200function combineSignals(signal, lifecycleSignal) { 1201 return signal ? AbortSignal.any([signal, lifecycleSignal]) : lifecycleSignal; 1202} 1203async function waitFor(pending, signal) { 1204 if (!signal) return pending; 1205 if (signal.aborted) throw signal.reason; 1206 return new Promise((resolve2, reject) => { 1207 const onAbort = () => reject(signal.reason); 1208 signal.addEventListener("abort", onAbort, { once: true }); 1209 pending.then( 1210 (value) => { 1211 signal.removeEventListener("abort", onAbort); 1212 resolve2(value); 1213 }, 1214 (error) => { 1215 signal.removeEventListener("abort", onAbort); 1216 reject(error); 1217 } 1218 ); 1219 }); 1220} 1221function assertSameOrigin(endpoint, origin) { 1222 if (!origin) return endpoint; 1223 try { 1224 const url = new URL(endpoint, origin); 1225 if (url.origin !== origin) { 1226 throw new Error( 1227 `MCP endpoint "${endpoint}" is cross-origin; only same-origin endpoints are allowed.` 1228 ); 1229 } 1230 return url.toString(); 1231 } catch (error) { 1232 if (error instanceof Error && error.message.includes("cross-origin")) { 1233 throw error; 1234 } 1235 throw new Error(`MCP endpoint "${endpoint}" is not a valid URL.`); 1236 } 1237} 1238function requestHeaders(protocolVersion, sessionId) { 1239 const headers = { 1240 "content-type": "application/json", 1241 accept: "application/json, text/event-stream", 1242 // Distinguish agent-mediated calls from user-initiated ones so the 1243 // site can attribute actions and apply different policy. 1244 "x-webmcp-agent": "1" 1245 }; 1246 if (protocolVersion) headers["MCP-Protocol-Version"] = protocolVersion; 1247 if (sessionId) headers["MCP-Session-Id"] = sessionId; 1248 return headers; 1249} 1250async function mcpRpcResponse(endpoint, origin, method, params, options = {}) { 1251 const rpcId = options.rpcId ?? RPC_ID; 1252 const res = await fetch(assertSameOrigin(endpoint, origin), { 1253 method: "POST", 1254 headers: options.headers ?? requestHeaders(), 1255 credentials: "same-origin", 1256 redirect: "error", 1257 body: JSON.stringify({ 1258 jsonrpc: "2.0", 1259 id: rpcId, 1260 method,
1261 params: options.requestParams ?? params 1262 }), 1263 signal: callSignal(options.signal) 1264 }); 1265 if (!res.ok) { 1266 let rpcError; 1267 const raw = await readBoundedText(res); 1268 if (raw) { 1269 try { 1270 const parsed = JSON.parse(raw); 1271 if (isObject(parsed) && isResponseEnvelope(parsed, rpcId)) { 1272 const error = parsed["error"]; 1273 if (isJsonRpcError(error)) rpcError = error; 1274 } 1275 } catch { 1276 } 1277 } 1278 throw new McpHttpError(res.status, rpcError); 1279 } 1280 const contentType = res.headers.get("content-type") ?? ""; 1281 const rpc = contentType.includes("text/event-stream") ? await readEventStreamResponse(res, rpcId) : parseJson(await readBoundedText(res)); 1282 if (!isResponseEnvelope(rpc, rpcId)) { 1283 throw new Error("MCP endpoint returned a mismatched JSON-RPC response"); 1284 } 1285 return { result: extractResult(rpc), headers: res.headers }; 1286} 1287async function mcpRpcNotification(endpoint, origin, method, signal, protocolVersion, sessionId) { 1288 const res = await fetch(assertSameOrigin(endpoint, origin), { 1289 method: "POST", 1290 headers: requestHeaders(protocolVersion, sessionId), 1291 credentials: "same-origin", 1292 redirect: "error", 1293 body: JSON.stringify({ jsonrpc: "2.0", method }), 1294 signal: callSignal(signal) 1295 }); 1296 if (!res.ok) throw new McpHttpError(res.status); 1297 await res.body?.cancel().catch(() => { 1298 }); 1299} 1300async function listTools(rpc, signal) { 1301 const out = []; 1302 let cursor; 1303 for (let page = 0; page < MCP_MAX_LIST_PAGES; page++) { 1304 const result = await rpc("tools/list", cursor ? { cursor } : {}, signal); 1305 if (!Array.isArray(result.tools)) { 1306 throw new Error("MCP tools/list response has no tools array"); 1307 } 1308 for (const tool of result.tools) { 1309 const definition = toToolDef(tool); 1310 if (definition) { 1311 out.push(definition); 1312 if (out.length >= MCP_MAX_TOOLS) { 1313 console.warn( 1314 `[webmcp-interceptor] MCP endpoint returned >= ${MCP_MAX_TOOLS} tools; stopping discovery early.` 1315 ); 1316 return out; 1317 } 1318 } 1319 } 1320 cursor = typeof result.nextCursor === "string" && result.nextCursor.length > 0 ? result.nextCursor : void 0; 1321 if (!cursor) break; 1322 } 1323 return out; 1324} 1325function toToolDef(value) { 1326 if (!isObject(value)) return null; 1327 if (typeof value.name !== "string" || value.name.trim().length === 0) return null; 1328 const definition = { name: value.name }; 1329 if (typeof value.description === "string" && value.description.trim().length > 0) { 1330 definition.description = value.description; 1331 } 1332 if (isObject(value.inputSchema) && value.inputSchema["type"] === "object") { 1333 definition.inputSchema = value.inputSchema; 1334 } 1335 return definition; 1336} 1337function toCallResult(result) { 1338 if (!Array.isArray(result.content)) { 1339 throw new Error("MCP tools/call result has no content array"); 1340 } 1341 let textTotal = 0; 1342 const cappedContent = result.content.filter(isContentBlock).map((block) => { 1343 if (isObject(block) && block["type"] === "text") { 1344 const text = block["text"]; 1345 if (typeof text === "string") { 1346 const remaining = MCP_MAX_CONTENT_TEXT_TOTAL_BYTES - textTotal; 1347 if (remaining <= 0) return { type: "text", text: "" }; 1348 let capped = text; 1349 if (encoder.encode(capped).length > MCP_MAX_CONTENT_TEXT_BYTES) { 1350 capped = sliceUtf8Bytes(capped, MCP_MAX_CONTENT_TEXT_BYTES); 1351 } 1352 if (encoder.encode(capped).length > remaining) { 1353 capped = sliceUtf8Bytes(capped, remaining); 1354 } 1355 textTotal += encoder.encode(capped).length; 1356 return { type: "text", text: capped }; 1357 } 1358 } 1359 return block; 1360 }); 1361 const out = { 1362 content: cappedContent, 1363 isError: result.isError === true 1364 }; 1365 if (isObject(result.structuredContent)) { 1366 try { 1367 const size = encoder.encode(JSON.stringify(result.structuredContent)).length; 1368 if (size <= MCP_MAX_STRUCTURED_CONTENT_BYTES) { 1369 out.structuredContent = result.structuredContent; 1370 } else { 1371 console.warn( 1372 `[webmcp-interceptor] mcp-server-client: structuredContent exceeds ${MCP_MAX_STRUCTURED_CONTENT_BYTES} bytes; dropping.` 1373 ); 1374 } 1375 } catch { 1376 } 1377 } 1378 return out; 1379} 1380function isContentBlock(value) { 1381 if (!isObject(value) || typeof value["type"] !== "string") return false;
1382 if (value["type"] === "text") return typeof value["text"] === "string"; 1383 return true; 1384} 1385function parseJson(raw) { 1386 try { 1387 return JSON.parse(raw); 1388 } catch { 1389 throw new Error("MCP endpoint returned a non-JSON response"); 1390 } 1391} 1392async function readEventStreamResponse(res, rpcId = RPC_ID) { 1393 const body = res.body; 1394 if (!body) return parseEventStream(await res.text()); 1395 const reader = body.getReader(); 1396 const decoder = new TextDecoder(); 1397 let tail = ""; 1398 let total = 0; 1399 try { 1400 for (; ; ) { 1401 const { done, value } = await reader.read(); 1402 if (done) break; 1403 if (!value) continue; 1404 total += value.byteLength; 1405 if (total > MCP_MAX_BYTES) { 1406 throw new Error(`MCP response exceeded ${MCP_MAX_BYTES} bytes; aborting.`); 1407 } 1408 const chunk = decoder.decode(value, { stream: true }); 1409 let position = 0; 1410 let newline; 1411 while ((newline = chunk.indexOf("\n", position)) >= 0) { 1412 const rawLine = tail + chunk.slice(position, newline); 1413 tail = ""; 1414 position = newline + 1; 1415 const envelope2 = tryParseDataLine(rawLine, rpcId); 1416 if (envelope2 !== void 0) return envelope2; 1417 } 1418 tail += chunk.slice(position); 1419 } 1420 const envelope = tryParseDataLine(tail, rpcId); 1421 if (envelope !== void 0) return envelope; 1422 } finally { 1423 await reader.cancel().catch(() => { 1424 }); 1425 } 1426 throw new Error("MCP event stream contained no JSON-RPC response"); 1427} 1428function parseEventStream(raw) { 1429 for (const rawLine of raw.split(/\r?\n/)) { 1430 const envelope = tryParseDataLine(rawLine, RPC_ID); 1431 if (envelope !== void 0) return envelope; 1432 } 1433 throw new Error("MCP event stream contained no JSON-RPC response"); 1434} 1435function tryParseDataLine(rawLine, rpcId) { 1436 const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine; 1437 if (!line.startsWith("data:")) return void 0; 1438 const payload = line.slice("data:".length).trim(); 1439 if (!payload) return void 0; 1440 try { 1441 const parsed = JSON.parse(payload); 1442 if (isResponseEnvelope(parsed, rpcId)) return parsed; 1443 } catch { 1444 } 1445 return void 0; 1446} 1447function isResponseEnvelope(value, rpcId) { 1448 if (!isObject(value) || value["jsonrpc"] !== "2.0") return false; 1449 const id = value["id"]; 1450 return id === rpcId || id === null && isJsonRpcError(value["error"]); 1451} 1452function isJsonRpcError(value) { 1453 return isObject(value) && typeof value["code"] === "number" && Number.isInteger(value["code"]) && typeof value["message"] === "string"; 1454} 1455function extractResult(rpc) { 1456 if (!isObject(rpc)) { 1457 throw new Error("Malformed JSON-RPC response from MCP endpoint"); 1458 } 1459 if ("error" in rpc) { 1460 const error = rpc.error; 1461 if (!isJsonRpcError(error)) { 1462 throw new Error("MCP endpoint returned a malformed JSON-RPC error"); 1463 } 1464 throw new Error(`MCP endpoint error: ${error["message"]}`); 1465 } 1466 const result = rpc.result; 1467 if (!isObject(result)) throw new Error("MCP response missing result"); 1468 return result; 1469} 1470async function terminateMcpSession(endpoint, origin, protocolVersion, sessionId) { 1471 try { 1472 await fetch(assertSameOrigin(endpoint, origin), { 1473 method: "DELETE", 1474 headers: requestHeaders(protocolVersion, sessionId), 1475 credentials: "same-origin", 1476 redirect: "error", 1477 keepalive: true, 1478 signal: AbortSignal.timeout(MCP_TIMEOUT_MS) 1479 }); 1480 } catch { 1481 } 1482} 1483
1484// src/bridge/packs/mcp-server-client/initialized-client.ts 1485var MCP_INITIALIZE_MAX_ATTEMPTS = 2; 1486var MCP_MAX_SESSION_ID_LENGTH = 512; 1487var MCP_SUPPORTED_PROTOCOL_VERSIONS = [ 1488 "2025-11-25", 1489 "2025-06-18", 1490 "2025-03-26" 1491]; 1492var InitializedClient = class { 1493 constructor(options) { 1494 this.options = options; 1495 } 1496 options; 1497 protocolVersion; 1498 sessionId; 1499 initializing; 1500 get negotiatedProtocolVersion() { 1501 return this.protocolVersion; 1502 } 1503 async connect(signal) { 1504 await this.ensureInitialized(signal); 1505 } 1506 async listTools(signal) { 1507 return listTools( 1508 (method, params, requestSignal) => this.sessionRpc(method, params, requestSignal), 1509 signal 1510 ); 1511 } 1512 async callTool(name, args, signal) { 1513 return toCallResult( 1514 await this.sessionRpc("tools/call", { name, arguments: args }, signal) 1515 ); 1516 } 1517 async close() { 1518 const sessionId = this.sessionId; 1519 const protocolVersion = this.protocolVersion; 1520 this.clearSession(); 1521 if (!sessionId || !protocolVersion) return; 1522 await terminateMcpSession( 1523 this.options.endpoint, 1524 this.options.origin, 1525 protocolVersion, 1526 sessionId 1527 ); 1528 } 1529 async ensureInitialized(signal) { 1530 this.options.assertOpen(); 1531 if (this.protocolVersion) return; 1532 this.initializing ??= this.initialize().finally(() => { 1533 this.initializing = void 0; 1534 }); 1535 await waitFor(this.initializing, signal); 1536 this.options.assertOpen(); 1537 } 1538 async initialize() { 1539 for (let attempt = 0; attempt < MCP_INITIALIZE_MAX_ATTEMPTS; attempt++) { 1540 this.options.assertOpen(); 1541 const response = await mcpRpcResponse( 1542 this.options.endpoint, 1543 this.options.origin, 1544 "initialize", 1545 { 1546 protocolVersion: MCP_SUPPORTED_PROTOCOL_VERSIONS[0], 1547 capabilities: {}, 1548 clientInfo: MCP_CLIENT_INFO 1549 }, 1550 { 1551 signal: this.options.lifecycleSignal, 1552 rpcId: this.options.nextRpcId() 1553 } 1554 ); 1555 const sessionId = readSessionId(response.headers); 1556 const protocolVersion = response.result["protocolVersion"]; 1557 try { 1558 this.options.assertOpen(); 1559 } catch (error) { 1560 if (sessionId) { 1561 void terminateMcpSession( 1562 this.options.endpoint, 1563 this.options.origin, 1564 typeof protocolVersion === "string" ? protocolVersion : MCP_SUPPORTED_PROTOCOL_VERSIONS[0], 1565 sessionId 1566 ); 1567 } 1568 throw error; 1569 } 1570 try { 1571 if (typeof protocolVersion !== "string" || !MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) { 1572 throw new Error( 1573 `MCP server negotiated unsupported protocol version: ${String(protocolVersion)}` 1574 ); 1575 } 1576 const capabilities = response.result["capabilities"]; 1577 if (!isObject(capabilities) || !isObject(capabilities["tools"])) { 1578 throw new Error("MCP server does not advertise the tools capability"); 1579 } 1580 } catch (error) { 1581 if (sessionId) { 1582 void terminateMcpSession( 1583 this.options.endpoint, 1584 this.options.origin, 1585 typeof protocolVersion === "string" ? protocolVersion : MCP_SUPPORTED_PROTOCOL_VERSIONS[0], 1586 sessionId 1587 ); 1588 } 1589 throw error; 1590 } 1591 try { 1592 await mcpRpcNotification( 1593 this.options.endpoint, 1594 this.options.origin, 1595 "notifications/initialized", 1596 this.options.lifecycleSignal, 1597 protocolVersion, 1598 sessionId 1599 ); 1600 this.options.assertOpen(); 1601 this.protocolVersion = protocolVersion; 1602 this.sessionId = sessionId; 1603 return; 1604 } catch (error) { 1605 const sessionMissing = error instanceof McpHttpError && error.status === 404 && sessionId !== void 0; 1606 if (sessionMissing && attempt + 1 < MCP_INITIALIZE_MAX_ATTEMPTS) continue; 1607 if (sessionId && !sessionMissing) { 1608 void terminateMcpSession( 1609 this.options.endpoint, 1610 this.options.origin, 1611 protocolVersion, 1612 sessionId 1613 ); 1614 } 1615 throw error; 1616 } 1617 } 1618 } 1619 async sessionRpc(method, params, signal) { 1620 await this.ensureInitialized(signal); 1621 this.options.assertOpen(); 1622 const sessionId = this.sessionId; 1623 try { 1624 return await this.rpcWithCurrentSession(method, params, signal); 1625 } catch (error) { 1626 if (!(error instanceof McpHttpError) || error.status !== 404 || !sessionId) { 1627 throw error; 1628 } 1629 this.options.assertOpen(); 1630 if (this.sessionId === sessionId) this.clearSession(); 1631 await this.ensureInitialized(signal); 1632 return this.rpcWithCurrentSession(method, params, signal); 1633 } 1634 } 1635 async rpcWithCurrentSession(method, params, signal) { 1636 const protocolVersion = this.protocolVersion; 1637 if (!protocolVersion) throw new Error("MCP client is not initialized"); 1638 this.options.assertOpen(); 1639 return (await mcpRpcResponse( 1640 this.options.endpoint, 1641 this.options.origin, 1642 method, 1643 params, 1644 { 1645 signal: combineSignals(signal, this.options.lifecycleSignal), 1646 headers: requestHeaders(protocolVersion, this.sessionId), 1647 rpcId: this.options.nextRpcId() 1648 } 1649 )).result; 1650 } 1651 clearSession() { 1652 this.protocolVersion = void 0; 1653 this.sessionId = void 0; 1654 } 1655}; 1656function readSessionId(headers) { 1657 const sessionId = headers.get("MCP-Session-Id"); 1658 if (sessionId === null) return void 0; 1659 if (sessionId.length === 0 || sessionId.length > MCP_MAX_SESSION_ID_LENGTH || !/^[\x21-\x7e]+$/.test(sessionId)) {
1660 throw new Error("MCP endpoint returned an invalid session ID"); 1661 } 1662 return sessionId; 1663} 1664
1665// src/bridge/packs/mcp-server-client/per-request-client.ts 1666var MCP_PER_REQUEST_PROTOCOL_VERSION = "2026-07-28"; 1667var MCP_UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE = -32022; 1668var MCP_MAX_HEADER_BINDINGS = 64; 1669var MCP_MAX_HEADER_VALUE_BYTES = 8 * 1024; 1670var MCP_MAX_SCHEMA_PROPERTIES = 256; 1671var headerEncoder = new TextEncoder(); 1672var InitializedFallbackRequired = class extends Error { 1673}; 1674var PerRequestProtocolSelectedError = class extends Error { 1675 constructor(originalError) { 1676 super("Per-request MCP protocol was selected before a later request failed"); 1677 this.originalError = originalError; 1678 } 1679 originalError; 1680}; 1681var PerRequestClient = class { 1682 constructor(options) { 1683 this.options = options; 1684 } 1685 options; 1686 tools = /* @__PURE__ */ new Map(); 1687 async discoverTools() { 1688 let responseReceived = false; 1689 try { 1690 const tools = await listTools( 1691 async (method, params, signal) => { 1692 return this.rpc(method, params, signal, () => { 1693 responseReceived = true; 1694 }); 1695 }, 1696 this.options.lifecycleSignal 1697 ); 1698 return this.rememberTools(tools); 1699 } catch (error) { 1700 if (responseReceived) throw new PerRequestProtocolSelectedError(error); 1701 if (!responseReceived && shouldFallbackToInitializedMode(error)) { 1702 throw new InitializedFallbackRequired(); 1703 } 1704 throw error; 1705 } 1706 } 1707 async listTools(signal) { 1708 return this.rememberTools( 1709 await listTools( 1710 (method, params, requestSignal) => this.rpc(method, params, requestSignal), 1711 signal 1712 ) 1713 ); 1714 } 1715 async callTool(name, args, signal) { 1716 return toCallResult( 1717 await this.rpc("tools/call", { name, arguments: args }, signal) 1718 ); 1719 } 1720 async rpc(method, params, signal, onResponse) { 1721 this.options.assertOpen(); 1722 const toolName = typeof params["name"] === "string" ? params["name"] : void 0; 1723 const response = await mcpRpcResponse( 1724 this.options.endpoint, 1725 this.options.origin, 1726 method, 1727 params, 1728 { 1729 signal: combineSignals(signal, this.options.lifecycleSignal), 1730 rpcId: this.options.nextRpcId(), 1731 headers: buildPerRequestHeaders( 1732 requestHeaders(MCP_PER_REQUEST_PROTOCOL_VERSION), 1733 method, 1734 params, 1735 toolName ? this.tools.get(toolName) : void 0 1736 ), 1737 requestParams: { 1738 ...params, 1739 _meta: { 1740 "io.modelcontextprotocol/protocolVersion": MCP_PER_REQUEST_PROTOCOL_VERSION, 1741 "io.modelcontextprotocol/clientInfo": MCP_CLIENT_INFO, 1742 "io.modelcontextprotocol/clientCapabilities": {} 1743 } 1744 } 1745 } 1746 ); 1747 onResponse?.(); 1748 const resultType = response.result["resultType"]; 1749 if (resultType !== void 0 && resultType !== "complete") { 1750 throw new Error( 1751 `MCP endpoint returned unsupported resultType: ${String(resultType)}` 1752 ); 1753 } 1754 return response.result; 1755 } 1756 rememberTools(tools) { 1757 this.tools.clear(); 1758 return tools.filter((tool) => { 1759 const headerError = validatePerRequestHeaderSchema(tool.inputSchema); 1760 if (headerError) { 1761 console.warn( 1762 `[webmcp-interceptor] mcp-server-client: skipping ${tool.name}: ${headerError}.` 1763 ); 1764 return false; 1765 } 1766 this.tools.set(tool.name, tool); 1767 return true; 1768 }); 1769 } 1770}; 1771function shouldFallbackToInitializedMode(error) { 1772 if (!(error instanceof McpHttpError) || error.status !== 400) return false; 1773 if (error.rpcError?.code !== MCP_UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE) { 1774 return true; 1775 } 1776 if (!isObject(error.rpcError.data)) return true; 1777 const supported = error.rpcError.data["supported"]; 1778 if (!Array.isArray(supported) || supported.length === 0 || !supported.every((version) => typeof version === "string")) { 1779 return true; 1780 } 1781 if (supported.includes(MCP_PER_REQUEST_PROTOCOL_VERSION)) return false; 1782 const perRequestVersions = supported.filter( 1783 (version) => version >= MCP_PER_REQUEST_PROTOCOL_VERSION 1784 ); 1785 if (perRequestVersions.length > 0) { 1786 console.warn( 1787 `[webmcp-interceptor] mcp-server-client: server advertised unsupported MCP protocol version(s) ${perRequestVersions.join(", ")}; this bridge supports ${MCP_PER_REQUEST_PROTOCOL_VERSION}.` 1788 ); 1789 return false;
1790 } 1791 return true; 1792} 1793function encodeHeaderValue(value) { 1794 const bytes = headerEncoder.encode(value); 1795 let encoded; 1796 if (value.length > 0 && value === value.trim() && /^[\x20-\x7e]+$/.test(value) && !(value.startsWith("=?base64?") && value.endsWith("?="))) { 1797 encoded = value; 1798 } else { 1799 let binary = ""; 1800 for (const byte of bytes) binary += String.fromCharCode(byte); 1801 encoded = `=?base64?${btoa(binary)}?=`; 1802 } 1803 if (encoded.length > MCP_MAX_HEADER_VALUE_BYTES) { 1804 throw new Error("MCP header value exceeds size limit"); 1805 } 1806 return encoded; 1807} 1808function readHeaderBindings(schema) { 1809 if (!schema || !isObject(schema["properties"])) return { bindings: [] }; 1810 const bindings = []; 1811 const headerNames = /* @__PURE__ */ new Set(); 1812 const properties = Object.entries(schema["properties"]); 1813 if (properties.length > MCP_MAX_SCHEMA_PROPERTIES) { 1814 return { bindings: [], error: "too many schema properties" }; 1815 } 1816 for (const [argumentName, property] of properties) { 1817 if (!isObject(property) || property["x-mcp-header"] === void 0) continue; 1818 const headerName = property["x-mcp-header"]; 1819 const type = property["type"]; 1820 if (typeof headerName !== "string" || !/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(headerName) || type !== "string" && type !== "integer" && type !== "boolean") { 1821 return { bindings: [], error: "invalid x-mcp-header annotation" }; 1822 } 1823 const normalizedName = headerName.toLowerCase(); 1824 if (headerNames.has(normalizedName)) { 1825 return { bindings: [], error: "duplicate x-mcp-header annotation" }; 1826 } 1827 if (bindings.length >= MCP_MAX_HEADER_BINDINGS) { 1828 return { bindings: [], error: "too many x-mcp-header annotations" }; 1829 } 1830 headerNames.add(normalizedName); 1831 bindings.push({ headerName, argumentName, type }); 1832 } 1833 return { bindings }; 1834} 1835function validatePerRequestHeaderSchema(schema) { 1836 return readHeaderBindings(schema).error; 1837} 1838function buildPerRequestHeaders(baseHeaders, method, params, tool) { 1839 const headers = { 1840 ...baseHeaders, 1841 "Mcp-Method": method 1842 }; 1843 if (typeof params["name"] === "string") { 1844 headers["Mcp-Name"] = encodeHeaderValue(params["name"]); 1845 } 1846 if (!tool || !isObject(params["arguments"])) return headers; 1847 const { bindings, error } = readHeaderBindings(tool.inputSchema); 1848 if (error) throw new Error(`${tool.name}: ${error}`); 1849 for (const binding of bindings) { 1850 if (!Object.hasOwn(params["arguments"], binding.argumentName)) continue; 1851 const value = params["arguments"][binding.argumentName]; 1852 if (value === void 0 || value === null) continue; 1853 const valid = binding.type === "string" && typeof value === "string" || binding.type === "boolean" && typeof value === "boolean" || binding.type === "integer" && typeof value === "number" && Number.isSafeInteger(value); 1854 if (!valid) throw new Error(`${tool.name}: invalid ${binding.argumentName}`); 1855 headers[`Mcp-Param-${binding.headerName}`] = encodeHeaderValue(String(value)); 1856 } 1857 return headers; 1858} 1859
1860// src/bridge/packs/mcp-server-client/mcp-client.ts 1861var McpHttpClient = class { 1862 selectedClient; 1863 negotiating; 1864 pagehideHandler; 1865 lifecycleController = new AbortController(); 1866 perRequestClient; 1867 initializedClient; 1868 closed = false; 1869 rpcId = 0; 1870 constructor(endpoint, origin) { 1871 const options = { 1872 endpoint, 1873 origin, 1874 lifecycleSignal: this.lifecycleController.signal, 1875 nextRpcId: () => this.nextRpcId(), 1876 assertOpen: () => this.assertOpen() 1877 }; 1878 this.perRequestClient = new PerRequestClient(options); 1879 this.initializedClient = new InitializedClient(options); 1880 } 1881 async listTools(signal) { 1882 const initialTools = await this.ensureClient(signal); 1883 if (initialTools) return initialTools; 1884 return this.requireSelectedClient().listTools(signal); 1885 } 1886 async callTool(name, args, signal) { 1887 await this.ensureClient(signal); 1888 return this.requireSelectedClient().callTool(name, args, signal); 1889 } 1890 attachPageLifecycle() { 1891 if (typeof window === "undefined" || this.pagehideHandler || this.closed) return; 1892 this.pagehideHandler = (event) => { 1893 if (event.persisted) return; 1894 if (this.pagehideHandler) { 1895 window.removeEventListener("pagehide", this.pagehideHandler); 1896 this.pagehideHandler = void 0; 1897 } 1898 void this.close(); 1899 }; 1900 window.addEventListener("pagehide", this.pagehideHandler); 1901 } 1902 async close() { 1903 if (this.closed) return; 1904 this.closed = true; 1905 this.lifecycleController.abort(); 1906 if (typeof window !== "undefined" && this.pagehideHandler) { 1907 window.removeEventListener("pagehide", this.pagehideHandler); 1908 this.pagehideHandler = void 0; 1909 } 1910 await this.initializedClient.close(); 1911 } 1912 async ensureClient(signal) { 1913 this.assertOpen(); 1914 if (this.selectedClient) return void 0; 1915 this.negotiating ??= this.negotiate().finally(() => { 1916 this.negotiating = void 0; 1917 }); 1918 const tools = await waitFor(this.negotiating, signal); 1919 this.assertOpen(); 1920 return tools; 1921 } 1922 async negotiate() { 1923 try { 1924 const tools = await this.perRequestClient.discoverTools(); 1925 this.assertOpen(); 1926 this.selectedClient = this.perRequestClient; 1927 return tools; 1928 } catch (error) { 1929 if (error instanceof PerRequestProtocolSelectedError) { 1930 this.selectedClient = this.perRequestClient; 1931 throw error.originalError; 1932 } 1933 if (!(error instanceof InitializedFallbackRequired)) throw error; 1934 await this.initializedClient.connect(); 1935 this.assertOpen(); 1936 this.selectedClient = this.initializedClient; 1937 console.warn( 1938 `[webmcp-interceptor] mcp-server-client: falling back from MCP ${MCP_PER_REQUEST_PROTOCOL_VERSION} to initialized MCP ${this.initializedClient.negotiatedProtocolVersion}.` 1939 ); 1940 return void 0; 1941 } 1942 } 1943 requireSelectedClient() { 1944 if (!this.selectedClient) { 1945 throw new Error("MCP client has not selected a protocol mode"); 1946 } 1947 return this.selectedClient; 1948 } 1949 assertOpen() { 1950 if (this.closed) throw new Error("MCP client is closed"); 1951 } 1952 nextRpcId() { 1953 this.rpcId += 1; 1954 return this.rpcId; 1955 } 1956}; 1957
1958// src/bridge/packs/mcp-server-client/index.ts 1959function isObject2(v) { 1960 return typeof v === "object" && v !== null; 1961} 1962var MAX_DESCRIPTION_CHARS = 4096; 1963var MAX_SCHEMA_BYTES = 32 * 1024; 1964var encoder2 = new TextEncoder(); 1965var SITE_TOOL_NOTE = "[Site-provided MCP tool \u2014 executes with your session credentials.] "; 1966function clampDescription(desc) { 1967 return desc.length > MAX_DESCRIPTION_CHARS ? desc.slice(0, MAX_DESCRIPTION_CHARS) : desc; 1968} 1969function safeInputSchema(schema) { 1970 if (!isObject2(schema) || schema["type"] !== "object") { 1971 return { type: "object", properties: {} }; 1972 } 1973 try { 1974 const size = encoder2.encode(JSON.stringify(schema)).length; 1975 if (size > MAX_SCHEMA_BYTES) { 1976 console.warn( 1977 `[webmcp-interceptor] mcp-server-client: tool inputSchema exceeds ${MAX_SCHEMA_BYTES} bytes; using empty schema.` 1978 ); 1979 return { type: "object", properties: {} }; 1980 } 1981 } catch { 1982 return { type: "object", properties: {} }; 1983 } 1984 return schema; 1985} 1986function makeHandler(client, toolName) { 1987 return async (rawArgs, ctx) => { 1988 const args = isObject2(rawArgs) ? rawArgs : {}; 1989 try { 1990 const result = await client.callTool(toolName, args, ctx.signal); 1991 const out = { 1992 content: result.content 1993 }; 1994 if (isObject2(result.structuredContent)) { 1995 out.structuredContent = result.structuredContent; 1996 } 1997 if (result.isError) out.isError = true; 1998 return customResult(out); 1999 } catch (err) { 2000 return errorResult(err instanceof Error ? err.message : String(err)); 2001 } 2002 }; 2003} 2004async function resolve(ctx) { 2005 const endpoint = ctx.mcpUrl; 2006 if (!endpoint) { 2007 return { tools: [], handlers: {} }; 2008 } 2009 let defs; 2010 const client = new McpHttpClient(endpoint, ctx.origin); 2011 client.attachPageLifecycle(); 2012 try { 2013 defs = await client.listTools(ctx.signal); 2014 } catch (err) { 2015 console.warn( 2016 `[webmcp-interceptor] mcp-server-client: tools/list failed for "${endpoint}"; registering no site tools.`, 2017 err 2018 ); 2019 void client.close(); 2020 return { tools: [], handlers: {} }; 2021 } 2022 if (defs.length === 0) { 2023 void client.close(); 2024 return { tools: [], handlers: {} }; 2025 } 2026 const tools = []; 2027 const handlers = {}; 2028 for (const def of defs) { 2029 tools.push({ 2030 name: def.name, 2031 // Prepend a provenance note so the agent can distinguish site- 2032 // provided tools from built-ins, and cap the description length so 2033 // a hostile endpoint can't flood the agent via an oversized 2034 // description. 2035 description: SITE_TOOL_NOTE + clampDescription(def.description || def.name), 2036 inputSchema: safeInputSchema(def.inputSchema) 2037 }); 2038 handlers[def.name] = makeHandler(client, def.name); 2039 } 2040 console.debug( 2041 `[webmcp-interceptor] mcp-server-client: registered ${tools.length} site tool(s) from ${endpoint}.` 2042 ); 2043 return { tools, handlers }; 2044} 2045var mcpServerClientPack = { 2046 kind: "dynamic", 2047 name: "mcp-server-client", 2048 resolve 2049}; 2050
2051// src/bridge/bridge.ts 2052var ALL_PACKS = [c2paPack, mcpServerClientPack]; 2053var DEFAULT_ACTIVE_PACKS = []; 2054function resolveOwnScript() { 2055 const candidates = Array.from( 2056 document.querySelectorAll("script[src]") 2057 ); 2058 try { 2059 const selfUrl = import.meta.url; 2060 const exact = candidates.find((s) => s.src === selfUrl); 2061 if (exact) return exact; 2062 } catch { 2063 } 2064 return candidates.find((s) => s.src.includes("/.webmcp/bridge.js")) ?? null; 2065} 2066var script = resolveOwnScript(); 2067initBridge().catch((err) => { 2068 console.error("[webmcp-interceptor] Bridge initialisation failed:", err); 2069}); 2070async function initBridge() { 2071 const mc = resolveModelContext(); 2072 if (!mc) { 2073 console.warn( 2074 "[webmcp-interceptor] document.modelContext is not available.\nTo enable WebMCP, use Chrome M146+ and enable:\nchrome://flags/#enable-experimental-web-platform-features" 2075 ); 2076 return; 2077 } 2078 const workerBaseUrl = resolveWorkerBaseUrl(); 2079 const mcpUrl = normalizeMcpUrl(script?.dataset["mcpUrl"]); 2080 const ctx = { 2081 origin: window.location.origin, 2082 workerRpc: workerBaseUrl ? makeWorkerRpc(workerBaseUrl) : workerRpcUnavailable, 2083 ...mcpUrl ? { mcpUrl } : {} 2084 }; 2085 const activePacks = selectActivePacks( 2086 ALL_PACKS, 2087 parsePackList(script?.dataset["packs"]), 2088 DEFAULT_ACTIVE_PACKS 2089 ); 2090 const staticResolved = resolveStaticPacks(activePacks); 2091 const staticRegistry = buildRegistry(staticResolved); 2092 registerTools(mc, ctx, staticRegistry); 2093 const registeredNames = new Set(staticRegistry.tools.map((t) => t.name)); 2094 console.debug( 2095 `[webmcp-interceptor] Registered ${staticRegistry.tools.length} static tool(s); resolving dynamic packs\u2026` 2096 ); 2097 const dynamicResolved = await resolveDynamicPacks(activePacks, ctx); 2098 if (dynamicResolved.length > 0) { 2099 const combined = buildRegistry([...staticResolved, ...dynamicResolved]); 2100 const additions = { 2101 tools: combined.tools.filter((t) => !registeredNames.has(t.name)), 2102 handlers: combined.handlers 2103 }; 2104 registerTools(mc, ctx, additions); 2105 console.debug( 2106 `[webmcp-interceptor] Registered ${additions.tools.length} dynamic tool(s) from ${dynamicResolved.length} dynamic pack(s).` 2107 ); 2108 } 2109} 2110function registerTools(mc, ctx, registry) { 2111 for (const tool of registry.tools) { 2112 const handler = registry.handlers[tool.name]; 2113 if (!handler) continue; 2114 mc.registerTool({ 2115 name: tool.name, 2116 // MCP's `Tool.description` is optional but WebMCP's 2117 // `registerTool()` requires a non-empty string. Fall back to 2118 // the tool name when the description is missing OR empty (`??` 2119 // alone would let an empty string through) so registration never 2120 // fails â the agent at least sees the name. 2121 description: tool.description || tool.name, 2122 inputSchema: tool.inputSchema, 2123 // WebMCP may pass an options bag with an AbortSignal as the 2124 // second argument; forward it so handlers can cancel in-flight 2125 // work. When absent, the per-call context simply has no signal. 2126 execute: (args, options) => invokeHandler(tool.name, handler, args, { 2127 ...ctx, 2128 ...options?.signal ? { signal: options.signal } : {} 2129 }) 2130 }); 2131 } 2132} 2133async function invokeHandler(toolName, handler, args, ctx) { 2134 let result; 2135 try { 2136 result = await handler(args, ctx); 2137 } catch (err) { 2138 const message = err instanceof Error ? err.message : String(err); 2139 console.warn(`[webmcp-interceptor] Tool "${toolName}" threw:`, message); 2140 throw new Error(message); 2141 } 2142 if (result.isError) { 2143 const text = extractText(result) ?? `${toolName} failed`; 2144 const error = new Error(text, { cause: result }); 2145 error.webmcpResult = result; 2146 throw error; 2147 } 2148 if (result.structuredContent !== void 0) return result.structuredContent; 2149 return extractText(result) ?? null; 2150} 2151function extractText(r) { 2152 if (!r.content || r.content.length === 0) return null; 2153 const texts = []; 2154 for (const block of r.content) { 2155 if (block.type === "text") texts.push(block.text); 2156 } 2157 return texts.length === 0 ? null : texts.join("\n"); 2158} 2159function resolveWorkerBaseUrl() { 2160 let src = script?.src; 2161 if (!src) { 2162 try { 2163 src = import.meta.url; 2164 } catch { 2165 src = void 0; 2166 } 2167 } 2168 if (!src) return null; 2169 try { 2170 const url = new URL(src); 2171 const webmcpIndex = url.pathname.indexOf("/.webmcp/"); 2172 if (webmcpIndex >= 0) { 2173 url.pathname = url.pathname.slice(0, webmcpIndex); 2174 } 2175 url.search = ""; 2176 url.hash = ""; 2177 return url.toString().replace(/\/$/, ""); 2178 } catch { 2179 return null; 2180 } 2181} 2182function makeWorkerRpc(baseUrl) { 2183 return async (path, body, validate) => { 2184 const url = `${baseUrl}/.webmcp/rpc/${path.replace(/^\//, "")}`; 2185 const res = await fetch(url, { 2186 method: "POST", 2187 headers: { "content-type": "application/json" }, 2188 // Important: NOT `credentials: 'include'`. The worker is on a 2189 // different origin and we don't want to forward the page's
2190 // cookies to it. Each pack is responsible for sending whatever 2191 // worker-specific auth it needs (today, none). 2192 body: JSON.stringify(body) 2193 }); 2194 const text = await res.text(); 2195 let parsed; 2196 try { 2197 parsed = text === "" ? null : JSON.parse(text); 2198 } catch { 2199 throw new Error(`Worker returned non-JSON response (HTTP ${res.status})`); 2200 } 2201 if (!res.ok) { 2202 const message = parsed && typeof parsed === "object" && "error" in parsed && typeof parsed.error === "string" ? parsed.error : `Worker RPC failed (HTTP ${res.status})`; 2203 throw new Error(message); 2204 } 2205 return validate(parsed); 2206 }; 2207} 2208function workerRpcUnavailable() { 2209 throw new Error( 2210 "Worker base URL could not be determined. Worker-backed tools are unavailable on this page." 2211 ); 2212} 2213function resolveModelContext() { 2214 const fromDocument = document.modelContext; 2215 if (fromDocument) return fromDocument; 2216 const fromNavigator = navigator.modelContext; 2217 return fromNavigator ?? null; 2218}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.