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 === "") return DEFAULT_MCP_URL; 31 return v; 32} 33function selectActivePacks(all, requested, defaults) { 34 const names = requested ?? defaults; 35 const byName = new Map(all.map((p) => [p.name, p])); 36 const active = []; 37 for (const name of names) { 38 const pack = byName.get(name); 39 if (pack) active.push(pack); 40 else 41 console.warn( 42 `[webmcp-interceptor] Requested pack "${name}" is not bundled; skipping.` 43 ); 44 } 45 return active; 46} 47function resolveStaticPacks(packs) { 48 const resolved = []; 49 for (const pack of packs) { 50 if (!isDynamicPack(pack)) { 51 resolved.push({ 52 name: pack.name, 53 tools: pack.tools, 54 handlers: pack.handlers, 55 dynamic: false 56 }); 57 } 58 } 59 return resolved; 60} 61async function resolveDynamicPacks(packs, ctx) { 62 const dynamics = packs.filter(isDynamicPack); 63 return Promise.all( 64 dynamics.map(async (pack) => { 65 try { 66 const { tools, handlers } = await pack.resolve(ctx); 67 return { name: pack.name, tools, handlers, dynamic: true }; 68 } catch (err) { 69 console.warn( 70 `[webmcp-interceptor] Dynamic pack "${pack.name}" failed to resolve; skipping.`, 71 err 72 ); 73 return { name: pack.name, tools: [], handlers: {}, dynamic: true }; 74 } 75 }) 76 ); 77} 78function buildRegistry(packs) { 79 const tools = []; 80 const handlers = {}; 81 const seen = /* @__PURE__ */ new Map(); 82 for (const pack of packs) { 83 for (const tool of pack.tools) { 84 if (!pack.handlers[tool.name]) { 85 throw new Error( 86 `Tool-pack "${pack.name}" declares tool "${tool.name}" but has no matching handler.` 87 ); 88 } 89 } 90 for (const name of Object.keys(pack.handlers)) { 91 if (!pack.tools.some((t) => t.name === name)) { 92 throw new Error( 93 `Tool-pack "${pack.name}" has a handler for "${name}" with no matching tool descriptor.` 94 ); 95 } 96 } 97 for (const tool of pack.tools) { 98 const existing = seen.get(tool.name); 99 if (existing) { 100 if (pack.dynamic) { 101 console.warn( 102 `[webmcp-interceptor] Tool "${tool.name}" from dynamic pack "${pack.name}" collides with "${existing}"; skipping the dynamic one.` 103 ); 104 continue; 105 } 106 throw new Error( 107 `Tool name collision: "${tool.name}" registered by both "${existing}" and "${pack.name}".` 108 ); 109 } 110 const handler = pack.handlers[tool.name]; 111 if (!handler) { 112 throw new Error( 113 `Tool-pack "${pack.name}" declares tool "${tool.name}" but has no matching handler.` 114 ); 115 } 116 seen.set(tool.name, pack.name); 117 tools.push(tool); 118 handlers[tool.name] = handler; 119 } 120 } 121 return { tools, handlers }; 122} 123
124// src/bridge/packs/c2pa/cbor.ts 125var BREAK = 255; 126var textDecoder = new TextDecoder("utf-8", { fatal: false }); 127function decodeCbor(bytes) { 128 return new CborDecoder(bytes).decode(); 129} 130function isCborTag(v) { 131 return typeof v === "object" && v !== null && !(v instanceof Uint8Array) && !Array.isArray(v) && !(v instanceof Map) && "tag" in v; 132} 133function isCborSimple(v) { 134 return typeof v === "object" && v !== null && !(v instanceof Uint8Array) && !Array.isArray(v) && !(v instanceof Map) && "simple" in v; 135} 136var CborDecoder = class { 137 constructor(bytes) { 138 this.bytes = bytes; 139 this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); 140 } 141 view; 142 off = 0; 143 decode() { 144 return this.readValue(0); 145 } 146 /** Read one item. `depth` guards against pathological nesting. */ 147 readValue(depth) { 148 if (depth > 256) throw new Error("CBOR nesting too deep"); 149 const ib = this.u8(); 150 const major = ib >> 5; 151 const ai = ib & 31; 152 switch (major) { 153 case 0: 154 return this.readUint(ai); 155 case 1: { 156 const n = this.readUint(ai); 157 return typeof n === "bigint" ? -1n - n : -1 - n; 158 } 159 case 2: 160 return this.readByteString(ai, depth); 161 case 3: 162 return textDecoder.decode(this.readByteString(ai, depth)); 163 case 4: 164 return this.readArray(ai, depth); 165 case 5: 166 return this.readMap(ai, depth); 167 case 6: { 168 const tag = this.asLength(this.readUint(ai)); 169 return { tag, value: this.readValue(depth + 1) }; 170 } 171 default: 172 return this.readSimple(ai); 173 } 174 } 175 /** Read the argument that follows the initial byte (major 0/1/6). */ 176 readUint(ai) { 177 if (ai < 24) return ai; 178 if (ai === 24) return this.u8(); 179 if (ai === 25) { 180 const v = this.view.getUint16(this.off); 181 this.off += 2; 182 return v; 183 } 184 if (ai === 26) { 185 const v = this.view.getUint32(this.off); 186 this.off += 4; 187 return v; 188 } 189 if (ai === 27) { 190 const v = this.view.getBigUint64(this.off); 191 this.off += 8; 192 return v <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(v) : v; 193 } 194 throw new Error(`Invalid CBOR additional-info value: ${ai}`); 195 } 196 readByteString(ai, depth) { 197 if (ai === 31) { 198 const chunks = []; 199 let total = 0; 200 for (; ; ) { 201 if (this.peek() === BREAK) { 202 this.off += 1; 203 break; 204 } 205 const chunk = this.readValue(depth + 1); 206 if (!(chunk instanceof Uint8Array)) { 207 throw new Error("Invalid indefinite-length byte string chunk"); 208 } 209 chunks.push(chunk); 210 total += chunk.length; 211 } 212 const out2 = new Uint8Array(total); 213 let p = 0; 214 for (const c of chunks) { 215 out2.set(c, p); 216 p += c.length; 217 } 218 return out2; 219 } 220 const len = this.asLength(this.readUint(ai)); 221 const end = this.off + len; 222 if (end > this.bytes.length) throw new Error("CBOR byte string truncated"); 223 const out = this.bytes.subarray(this.off, end); 224 this.off = end; 225 return out; 226 } 227 readArray(ai, depth) { 228 const out = []; 229 if (ai === 31) { 230 while (this.peek() !== BREAK) out.push(this.readValue(depth + 1)); 231 this.off += 1; 232 return out; 233 } 234 const len = this.asLength(this.readUint(ai)); 235 for (let i = 0; i < len; i++) out.push(this.readValue(depth + 1)); 236 return out; 237 } 238 readMap(ai, depth) { 239 const out = /* @__PURE__ */ new Map(); 240 if (ai === 31) { 241 while (this.peek() !== BREAK) { 242 const k = this.readValue(depth + 1); 243 out.set(k, this.readValue(depth + 1)); 244 } 245 this.off += 1; 246 return out; 247 } 248 const len = this.asLength(this.readUint(ai)); 249 for (let i = 0; i < len; i++) { 250 const k = this.readValue(depth + 1); 251 out.set(k, this.readValue(depth + 1)); 252 } 253 return out; 254 } 255 readSimple(ai) { 256 switch (ai) { 257 case 20: 258 return false; 259 case 21: 260 return true; 261 case 22: 262 return null; 263 case 23: 264 return void 0; 265 case 24:
266 return { simple: this.u8() }; 267 // one-byte simple value (32-255) 268 case 25: 269 return this.readHalfFloat(); 270 case 26: { 271 const v = this.view.getFloat32(this.off); 272 this.off += 4; 273 return v; 274 } 275 case 27: { 276 const v = this.view.getFloat64(this.off); 277 this.off += 8; 278 return v; 279 } 280 default: 281 return { simple: ai }; 282 } 283 } 284 /** IEEE 754 half-precision (binary16) â JS number. */ 285 readHalfFloat() { 286 const half = this.view.getUint16(this.off); 287 this.off += 2; 288 const exp = (half & 31744) >> 10; 289 const frac = half & 1023; 290 const sign = half & 32768 ? -1 : 1; 291 if (exp === 0) return sign * 2 ** -14 * (frac / 1024); 292 if (exp === 31) return frac ? NaN : sign * Infinity; 293 return sign * 2 ** (exp - 15) * (1 + frac / 1024); 294 } 295 u8() { 296 if (this.off >= this.bytes.length) throw new Error("CBOR truncated"); 297 const v = this.view.getUint8(this.off); 298 this.off += 1; 299 return v; 300 } 301 peek() { 302 if (this.off >= this.bytes.length) throw new Error("CBOR truncated"); 303 return this.view.getUint8(this.off); 304 } 305 /** Coerce a length/tag argument to a sane JS number or throw. */ 306 asLength(v) { 307 if (typeof v === "bigint" || v > 2147483647) { 308 throw new Error("CBOR length/tag exceeds supported range"); 309 } 310 return v; 311 } 312}; 313
314// src/bridge/packs/c2pa/x509.ts 315var textDecoder2 = new TextDecoder("utf-8", { fatal: false }); 316var OID_COMMON_NAME = [85, 4, 3]; 317function parseCertificate(der) { 318 try { 319 return parseCertificateUnsafe(der); 320 } catch { 321 return {}; 322 } 323} 324function parseCertificateUnsafe(der) { 325 const view = new DataView(der.buffer, der.byteOffset, der.byteLength); 326 const cert = readTlv(view, 0, der.length); 327 if (!cert) return {}; 328 const tbs = readTlv(view, cert.contentStart, cert.contentEnd); 329 if (!tbs) return {}; 330 const fields = readChildren(view, der, tbs.contentStart, tbs.contentEnd); 331 let i = 0; 332 if (fields[i] && fields[i]?.tag === 160) i += 1; 333 const issuer = fields[i + 2]; 334 const validity = fields[i + 3]; 335 const subject = fields[i + 4]; 336 const info = {}; 337 if (issuer) info.issuerCN = extractCommonName(view, der, issuer); 338 if (subject) info.subjectCN = extractCommonName(view, der, subject); 339 if (validity) { 340 const times = readChildren(view, der, validity.contentStart, validity.contentEnd); 341 if (times[0]) info.notBefore = parseTime(der, times[0]); 342 if (times[1]) info.notAfter = parseTime(der, times[1]); 343 } 344 return info; 345} 346function readTlv(view, off, limit) { 347 if (off + 2 > limit) return null; 348 const tag = view.getUint8(off); 349 let p = off + 1; 350 let len = view.getUint8(p); 351 p += 1; 352 if (len & 128) { 353 const n = len & 127; 354 if (n === 0 || n > 4) return null; 355 len = 0; 356 for (let i = 0; i < n; i++) { 357 len = len << 8 | view.getUint8(p); 358 p += 1; 359 } 360 } 361 const contentStart = p; 362 const contentEnd = p + len; 363 if (contentEnd > limit) return null; 364 return { tag, contentStart, contentEnd, end: contentEnd }; 365} 366function readChildren(view, bytes, start, end) { 367 const out = []; 368 let off = start; 369 while (off < end) { 370 const node = readTlv(view, off, end); 371 if (!node) break; 372 out.push(node); 373 off = node.end; 374 } 375 return out; 376} 377function extractCommonName(view, bytes, name) { 378 for (const rdn of readChildren(view, bytes, name.contentStart, name.contentEnd)) { 379 for (const atv of readChildren(view, bytes, rdn.contentStart, rdn.contentEnd)) { 380 const parts = readChildren(view, bytes, atv.contentStart, atv.contentEnd); 381 const oid = parts[0]; 382 const value = parts[1]; 383 if (oid && value && oid.tag === 6 && oidEquals(bytes, oid, OID_COMMON_NAME)) { 384 return textDecoder2.decode(bytes.subarray(value.contentStart, value.contentEnd)); 385 } 386 } 387 } 388 return void 0; 389} 390function oidEquals(bytes, node, expected) { 391 const len = node.contentEnd - node.contentStart; 392 if (len !== expected.length) return false; 393 for (let i = 0; i < len; i++) { 394 if (bytes[node.contentStart + i] !== expected[i]) return false; 395 } 396 return true; 397} 398function parseTime(bytes, node) { 399 const raw = textDecoder2.decode(bytes.subarray(node.contentStart, node.contentEnd)); 400 const isUtc = node.tag === 23; 401 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); 402 if (!m) return raw; 403 let year = Number(m[1]); 404 if (isUtc) year += year < 50 ? 2e3 : 1900; 405 const iso = `${pad(year, 4)}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6] ?? "00"}Z`; 406 return iso; 407} 408function pad(n, width) { 409 return String(n).padStart(width, "0"); 410} 411
412// src/bridge/packs/c2pa/manifest.ts 413function buildManifestStore(store) { 414 const manifests = store.children.map(decodeManifest); 415 const active = manifests[manifests.length - 1]; 416 const result = { 417 manifestCount: manifests.length, 418 manifests 419 }; 420 if (store.label !== void 0) result.label = store.label; 421 if (active?.label !== void 0) result.activeManifestLabel = active.label; 422 return result; 423} 424function decodeManifest(node) { 425 let claimNode; 426 let signatureNode; 427 let assertionStore; 428 for (const child of node.children) { 429 const label = child.label ?? ""; 430 if (label.startsWith("c2pa.claim") || child.contentType === "c2cl") { 431 claimNode = child; 432 } else if (label === "c2pa.signature" || child.contentType === "c2cs") { 433 signatureNode = child; 434 } else if (label === "c2pa.assertions" || child.contentType === "c2as") { 435 assertionStore = child; 436 } 437 } 438 const assertions = assertionStore ? assertionStore.children.map(decodeAssertion) : []; 439 const manifest = { assertions }; 440 if (node.label !== void 0) manifest.label = node.label; 441 if (claimNode?.cbor !== void 0) { 442 const claim = cborToJson(claimNode.cbor); 443 if (claim !== null && typeof claim === "object" && !Array.isArray(claim)) { 444 manifest.claim = claim; 445 } 446 } 447 if (signatureNode?.cbor !== void 0) { 448 manifest.signature = decodeCoseSignature(signatureNode.cbor); 449 } 450 return manifest; 451} 452function decodeAssertion(node) { 453 const out = {}; 454 if (node.label !== void 0) out.label = node.label; 455 if (node.cbor !== void 0) { 456 out.data = cborToJson(node.cbor); 457 } else if (node.json !== void 0) { 458 out.data = node.json; 459 } else if (node.binary !== void 0) { 460 out.binary = node.binary; 461 } 462 return out; 463} 464var COSE_ALG = { 465 [-7]: "ES256", 466 [-35]: "ES384", 467 [-36]: "ES512", 468 [-8]: "EdDSA", 469 [-37]: "PS256", 470 [-38]: "PS384", 471 [-39]: "PS512", 472 [-257]: "RS256", 473 [-258]: "RS384", 474 [-259]: "RS512" 475}; 476function decodeCoseSignature(cose) { 477 const arr = isCborTag(cose) ? cose.value : cose; 478 if (!Array.isArray(arr) || arr.length < 4) return {}; 479 const protectedHeader = decodeProtectedHeader(arr[0]); 480 const unprotected = arr[1] instanceof Map ? arr[1] : /* @__PURE__ */ new Map(); 481 const sig = {}; 482 const algId = asInt(protectedHeader.get(1) ?? unprotected.get(1)); 483 if (algId !== void 0) sig.alg = COSE_ALG[algId] ?? `COSE(${algId})`; 484 const certs = readX5Chain(protectedHeader.get(33) ?? unprotected.get(33)); 485 if (certs.length > 0) { 486 sig.certChainLength = certs.length; 487 const first = certs[0]; 488 if (first) { 489 const info = parseCertificate(first); 490 if (info.subjectCN !== void 0) sig.signedBy = info.subjectCN; 491 if (info.issuerCN !== void 0) sig.certifiedBy = info.issuerCN; 492 if (info.notBefore !== void 0) sig.certValidFrom = info.notBefore; 493 if (info.notAfter !== void 0) sig.certValidTo = info.notAfter; 494 } 495 } 496 return sig; 497} 498function decodeProtectedHeader(value) { 499 if (value instanceof Uint8Array && value.length > 0) { 500 const decoded = decodeCbor(value); 501 if (decoded instanceof Map) return decoded; 502 } 503 return /* @__PURE__ */ new Map(); 504} 505function readX5Chain(value) { 506 if (value instanceof Uint8Array) return [value]; 507 if (Array.isArray(value)) { 508 return value.filter((c) => c instanceof Uint8Array); 509 } 510 return []; 511} 512function asInt(value) { 513 if (typeof value === "number" && Number.isInteger(value)) return value; 514 if (typeof value === "bigint") return Number(value); 515 return void 0; 516} 517var MAX_INLINE_BYTES = 64; 518function cborToJson(value) { 519 if (value === null || value === void 0) return value ?? null; 520 if (typeof value === "bigint") return value.toString(); 521 if (value instanceof Uint8Array) return bytesToJson(value); 522 if (Array.isArray(value)) return value.map(cborToJson); 523 if (value instanceof Map) { 524 const obj = {}; 525 for (const [k, v] of value) obj[keyToString(k)] = cborToJson(v); 526 return obj; 527 } 528 if (isCborTag(value)) { 529 return { $tag: value.tag, value: cborToJson(value.value) }; 530 } 531 if (isCborSimple(value)) { 532 return { $simple: value.simple }; 533 } 534 return value; 535} 536function bytesToJson(b) { 537 if (b.length <= MAX_INLINE_BYTES) return { $bytes: b.length, hex: toHex(b) }; 538 return { $bytes: b.length, hex: `${toHex(b.subarray(0, 32))}\u2026` }; 539} 540function keyToString(k) { 541 if (typeof k === "string") return k; 542 if (typeof k === "number" || typeof k === "bigint" || typeof k === "boolean") { 543 return String(k); 544 } 545 return JSON.stringify(cborToJson(k)); 546} 547function toHex(b) { 548 let s = ""; 549 for (let i = 0; i < b.length; i++) { 550 const byte = b[i]; 551 if (byte === void 0) break; 552 s += byte.toString(16).padStart(2, "0"); 553 } 554 return s; 555} 556
557// src/bridge/packs/c2pa/jumbf.ts 558var textDecoder3 = new TextDecoder("utf-8", { fatal: false }); 559function detectImageFormat(bytes) { 560 if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) { 561 return "jpeg"; 562 } 563 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) { 564 return "png"; 565 } 566 return "unknown"; 567} 568function extractC2paJumbf(bytes) { 569 switch (detectImageFormat(bytes)) { 570 case "jpeg": 571 return extractFromJpeg(bytes); 572 case "png": 573 return extractFromPng(bytes); 574 default: 575 return null; 576 } 577} 578function parseManifestStore(jumbf) { 579 const view = new DataView(jumbf.buffer, jumbf.byteOffset, jumbf.byteLength); 580 const top = readBox(view, jumbf, 0, jumbf.length); 581 if (!top || top.type !== "jumb") return null; 582 return parseSuperbox(view, jumbf, top.payloadStart, top.payloadEnd, 0); 583} 584function extractFromJpeg(bytes) { 585 const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); 586 if (view.getUint16(0) !== 65496) return null; 587 const packets = /* @__PURE__ */ new Map(); 588 let off = 2; 589 while (off + 2 <= bytes.length) {
590 if (view.getUint8(off) !== 255) break; 591 let marker = view.getUint8(off + 1); 592 while (marker === 255 && off + 2 < bytes.length) { 593 off += 1; 594 marker = view.getUint8(off + 1); 595 } 596 off += 2; 597 if (marker === 217 || marker === 218) break; 598 if (marker === 1 || marker >= 208 && marker <= 215) continue; 599 if (off + 2 > bytes.length) break; 600 const segLen = view.getUint16(off); 601 const segStart = off + 2; 602 const segEnd = off + segLen; 603 if (segLen < 2 || segEnd > bytes.length) break; 604 if (marker === 235 && segEnd - segStart >= 8) { 605 if (view.getUint16(segStart) === 19024) { 606 const en = view.getUint16(segStart + 2); 607 const z = view.getUint32(segStart + 4); 608 const data = bytes.subarray(segStart + 8, segEnd); 609 const arr = packets.get(en) ?? []; 610 arr.push({ z, data }); 611 packets.set(en, arr); 612 } 613 } 614 off = segEnd; 615 } 616 for (const arr of packets.values()) { 617 arr.sort((a, b) => a.z - b.z); 618 const first = arr[0]; 619 if (!first) continue; 620 let headerLen = 8; 621 if (first.data.length >= 4) { 622 const fv = new DataView( 623 first.data.buffer, 624 first.data.byteOffset, 625 first.data.byteLength 626 ); 627 if (fv.getUint32(0) === 1) headerLen = 16; 628 } 629 const pieces = []; 630 let total = 0; 631 for (let i = 0; i < arr.length; i++) { 632 const pk = arr[i]; 633 if (!pk) continue; 634 const piece = i === 0 ? pk.data : pk.data.subarray(headerLen); 635 pieces.push(piece); 636 total += piece.length; 637 } 638 const buf = new Uint8Array(total); 639 let p = 0; 640 for (const piece of pieces) { 641 buf.set(piece, p); 642 p += piece.length; 643 } 644 if (isC2paStore(buf)) return buf; 645 } 646 return null; 647} 648function extractFromPng(bytes) { 649 const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); 650 let off = 8; 651 while (off + 8 <= bytes.length) { 652 const len = view.getUint32(off); 653 const type = ascii(bytes, off + 4, 4); 654 const dataStart = off + 8; 655 const dataEnd = dataStart + len; 656 if (dataEnd + 4 > bytes.length) break; 657 if (type === "caBX") { 658 const buf = bytes.subarray(dataStart, dataEnd); 659 return isC2paStore(buf) ? buf : null; 660 } 661 if (type === "IEND") break; 662 off = dataEnd + 4; 663 } 664 return null; 665} 666function readBox(view, bytes, offset, limit) { 667 if (offset + 8 > limit) return null; 668 let len = view.getUint32(offset); 669 const type = ascii(bytes, offset + 4, 4); 670 let headerLen = 8; 671 if (len === 1) { 672 if (offset + 16 > limit) return null; 673 const hi = view.getUint32(offset + 8); 674 const lo = view.getUint32(offset + 12); 675 len = hi * 4294967296 + lo; 676 headerLen = 16; 677 } else if (len === 0) { 678 len = limit - offset; 679 } 680 const boxEnd = offset + len; 681 if (len < headerLen || boxEnd > limit) return null; 682 return { type, payloadStart: offset + headerLen, payloadEnd: boxEnd, boxEnd }; 683} 684function parseSuperbox(view, bytes, start, end, depth) { 685 const node = { contentType: "", children: [] }; 686 if (depth > 64) return node; 687 let off = start; 688 const first = readBox(view, bytes, off, end); 689 if (first && first.type === "jumd") { 690 parseDescription(view, bytes, first.payloadStart, first.payloadEnd, node); 691 off = first.boxEnd; 692 } 693 while (off < end) { 694 const box = readBox(view, bytes, off, end); 695 if (!box) break; 696 switch (box.type) { 697 case "jumb": 698 node.children.push( 699 parseSuperbox(view, bytes, box.payloadStart, box.payloadEnd, depth + 1) 700 ); 701 break; 702 case "cbor": 703 try { 704 node.cbor = decodeCbor(bytes.subarray(box.payloadStart, box.payloadEnd)); 705 } catch { 706 } 707 break; 708 case "json": 709 try { 710 node.json = JSON.parse( 711 textDecoder3.decode(bytes.subarray(box.payloadStart, box.payloadEnd)) 712 ); 713 } catch { 714 } 715 break; 716 case "bidb": 717 // embedded-file data box 718 case "uuid": 719 node.binary = { size: box.payloadEnd - box.payloadStart }; 720 break; 721 default: 722 break; 723 } 724 off = box.boxEnd; 725 } 726 return node; 727} 728function parseDescription(view, bytes, start, end, node) { 729 if (start + 17 > end) return; 730 node.contentType = ascii(bytes, start, 4); 731 const toggles = view.getUint8(start + 16); 732 let off = start + 17; 733 if (toggles & 2) { 734 let z = off; 735 while (z < end && view.getUint8(z) !== 0) z += 1; 736 node.label = textDecoder3.decode(bytes.subarray(off, z)); 737 } 738} 739function isC2paStore(buf) { 740 if (buf.length < 16) return false;
741 const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); 742 const top = readBox(view, buf, 0, buf.length); 743 if (!top || top.type !== "jumb") return false; 744 const desc = readBox(view, buf, top.payloadStart, top.payloadEnd); 745 if (!desc || desc.type !== "jumd") return false; 746 return ascii(buf, desc.payloadStart, 4) === "c2pa"; 747} 748function ascii(bytes, off, len) { 749 let s = ""; 750 for (let i = 0; i < len; i++) { 751 const c = bytes[off + i]; 752 if (c === void 0) break; 753 s += String.fromCharCode(c); 754 } 755 return s; 756} 757
758// src/bridge/packs/c2pa/c2pa-executor.ts 759var MAX_SCAN_IMAGES = 100; 760var MAX_IMAGE_BYTES = 64 * 1024 * 1024; 761var FETCH_TIMEOUT_MS = 15e3; 762var SCAN_CONCURRENCY = 6; 763var SCAN_BUDGET_MS = 3e4; 764var NO_VERIFICATION_NOTE = "Manifest decoded only. Cryptographic signature, certificate-chain, and trust-list verification were NOT performed; treat provenance as unverified claims."; 765async function scanImages(limit, signal) { 766 const allSources = collectImageSources(); 767 const capped = Math.min(limit, MAX_SCAN_IMAGES); 768 const sources = allSources.slice(0, capped); 769 const deadline = anySignal(signal, AbortSignal.timeout(SCAN_BUDGET_MS)); 770 const results = await mapWithConcurrency( 771 sources, 772 SCAN_CONCURRENCY, 773 (src) => scanOne(src, deadline) 774 ); 775 const withC2pa = results.filter((r) => r.hasC2pa).length; 776 return { 777 imageCount: allSources.length, 778 scanned: results.length, 779 withC2pa, 780 results 781 }; 782} 783async function scanOne(src, signal) { 784 try { 785 const bytes = await fetchImageBytes(src, signal); 786 const analysis = analyzeBytes(bytes); 787 if (!analysis.store) { 788 return { src, hasC2pa: false, format: analysis.format }; 789 } 790 const active = activeManifest(analysis.store); 791 const summary = { 792 src, 793 hasC2pa: true, 794 format: analysis.format, 795 manifestCount: analysis.store.manifestCount 796 }; 797 const claimGenerator = readClaimGenerator(active?.claim); 798 const title = readTitle(active?.claim); 799 if (claimGenerator !== void 0) summary.claimGenerator = claimGenerator; 800 if (title !== void 0) summary.title = title; 801 if (active?.signature?.signedBy !== void 0) { 802 summary.signedBy = active.signature.signedBy; 803 } 804 return summary; 805 } catch (err) { 806 return { src, hasC2pa: false, error: errorMessage(err) }; 807 } 808} 809async function inspectImage(opts, signal) { 810 const src = resolveSource(opts); 811 const bytes = await fetchImageBytes(src, signal); 812 const analysis = analyzeBytes(bytes); 813 if (!analysis.store) { 814 return { 815 src, 816 hasC2pa: false, 817 format: analysis.format, 818 message: analysis.format === "unknown" ? "Unsupported image format (only JPEG and PNG are parsed today)." : "No C2PA manifest found in this image." 819 }; 820 } 821 return { 822 src, 823 hasC2pa: true, 824 format: analysis.format, 825 manifestStore: analysis.store, 826 validation: { signatureVerified: false, note: NO_VERIFICATION_NOTE } 827 }; 828} 829function analyzeBytes(bytes) { 830 const format = detectImageFormat(bytes); 831 const jumbf = extractC2paJumbf(bytes); 832 if (!jumbf) return { format }; 833 const tree = parseManifestStore(jumbf); 834 if (!tree) return { format }; 835 return { format, store: buildManifestStore(tree) }; 836} 837function collectImageSources() { 838 const seen = /* @__PURE__ */ new Set(); 839 const out = []; 840 for (const img of Array.from(document.querySelectorAll("img"))) { 841 const el = img; 842 const src = el.currentSrc || el.src; 843 if (src && !seen.has(src)) { 844 seen.add(src); 845 out.push(src); 846 } 847 } 848 return out; 849} 850function resolveSource(opts) { 851 if (opts.url) { 852 return new URL(opts.url, document.location.href).toString(); 853 } 854 if (opts.selector) { 855 const el = document.querySelector(opts.selector); 856 if (!el) throw new Error(`No element matches selector: ${opts.selector}`); 857 if (!(el instanceof HTMLImageElement)) { 858 throw new Error(`Element is not an <img>: ${opts.selector}`); 859 } 860 const src = el.currentSrc || el.src; 861 if (!src) throw new Error(`<img> has no resolved src: ${opts.selector}`); 862 return src; 863 } 864 throw new Error("Provide either a `selector` or a `url`."); 865} 866async function fetchImageBytes(src, signal) { 867 const combined = anySignal(signal, AbortSignal.timeout(FETCH_TIMEOUT_MS)); 868 let res; 869 try { 870 res = await fetch(src, { signal: combined }); 871 } catch (err) { 872 if (err instanceof DOMException && err.name === "TimeoutError") { 873 throw new Error("Image fetch timed out"); 874 } 875 if (err instanceof DOMException && err.name === "AbortError") { 876 throw new Error("Image fetch aborted"); 877 } 878 throw err; 879 } 880 if (!res.ok) throw new Error(`Fetch failed: HTTP ${res.status}`); 881 const length = Number(res.headers.get("content-length") ?? "0"); 882 if (length > MAX_IMAGE_BYTES) { 883 throw new Error(`Image too large to inspect (${length} bytes)`); 884 } 885 const buf = await res.arrayBuffer(); 886 if (buf.byteLength > MAX_IMAGE_BYTES) { 887 throw new Error(`Image too large to inspect (${buf.byteLength} bytes)`); 888 } 889 return new Uint8Array(buf); 890} 891function activeManifest(store) { 892 return store.manifests[store.manifests.length - 1]; 893} 894function readClaimGenerator(claim) { 895 if (!claim) return void 0; 896 const info = claim["claim_generator_info"]; 897 if (Array.isArray(info) && info.length > 0) { 898 const first = info[0]; 899 if (first && typeof first === "object") { 900 const name = first["name"]; 901 const version = first["version"]; 902 if (typeof name === "string") { 903 return typeof version === "string" ? `${name} ${version}` : name; 904 } 905 } 906 } 907 const generator = claim["claim_generator"]; 908 return typeof generator === "string" ? generator : void 0; 909} 910function readTitle(claim) { 911 if (!claim) return void 0; 912 const title = claim["dc:title"] ?? claim["title"]; 913 return typeof title === "string" ? title : void 0; 914} 915function errorMessage(err) {
916 return err instanceof Error ? err.message : String(err); 917} 918function anySignal(optional, required) { 919 return optional ? AbortSignal.any([optional, required]) : required; 920} 921async function mapWithConcurrency(items, concurrency, fn) { 922 const results = new Array(items.length); 923 let next = 0; 924 const workers = []; 925 const limit = Math.max(1, Math.min(concurrency, items.length)); 926 for (let w = 0; w < limit; w++) { 927 workers.push( 928 (async () => { 929 for (; ; ) { 930 const i = next++; 931 if (i >= items.length) return; 932 const item = items[i]; 933 if (item === void 0) continue; 934 results[i] = await fn(item); 935 } 936 })() 937 ); 938 } 939 await Promise.all(workers); 940 return results; 941} 942
943// src/bridge/packs/c2pa/index.ts 944var TOOLS = [ 945 { 946 name: "scan_images_c2pa", 947 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.", 948 inputSchema: { 949 type: "object", 950 properties: { 951 limit: { 952 type: "integer", 953 minimum: 1, 954 maximum: 100, 955 default: 25, 956 description: "Maximum number of images to fetch and scan (default 25, max 100)." 957 } 958 }, 959 required: [] 960 } 961 }, 962 { 963 name: "inspect_image_c2pa", 964 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.", 965 inputSchema: { 966 type: "object", 967 properties: { 968 selector: { 969 type: "string", 970 maxLength: 8192, 971 description: "CSS selector for an <img> element on the page." 972 }, 973 url: { 974 type: "string", 975 maxLength: 8192, 976 description: "Image URL (absolute, or relative to the current page) to fetch and inspect." 977 } 978 }, 979 required: [] 980 } 981 } 982]; 983function optionalInt(args, key, min, max, fallback) { 984 if (typeof args !== "object" || args === null) return fallback; 985 const v = args[key]; 986 if (v === void 0) return fallback; 987 if (typeof v !== "number" || !Number.isInteger(v)) return null; 988 if (v < min || v > max) return null; 989 return v; 990} 991var MAX_STRING_ARG = 8192; 992function readStringArg(args, key) { 993 if (typeof args !== "object" || args === null) return { kind: "absent" }; 994 const v = args[key]; 995 if (v === void 0 || v === null) return { kind: "absent" }; 996 if (typeof v !== "string") { 997 return { kind: "invalid", reason: `\`${key}\` must be a string.` }; 998 } 999 if (v.length === 0) return { kind: "absent" }; 1000 if (v.length > MAX_STRING_ARG) { 1001 return { 1002 kind: "invalid", 1003 reason: `\`${key}\` exceeds the ${MAX_STRING_ARG}-character limit.` 1004 }; 1005 } 1006 return { kind: "ok", value: v }; 1007} 1008var handleScanImages = async (rawArgs, ctx) => { 1009 const limit = optionalInt(rawArgs, "limit", 1, 100, 25); 1010 if (limit === null) return errorResult("Invalid tool arguments"); 1011 return ok({ ...await scanImages(limit, ctx.signal) }); 1012}; 1013var handleInspectImage = async (rawArgs, ctx) => { 1014 const selector = readStringArg(rawArgs, "selector"); 1015 const url = readStringArg(rawArgs, "url"); 1016 if (selector.kind === "invalid") return errorResult(selector.reason); 1017 if (url.kind === "invalid") return errorResult(url.reason); 1018 const hasSelector = selector.kind === "ok"; 1019 const hasUrl = url.kind === "ok"; 1020 if (!hasSelector && !hasUrl) { 1021 return errorResult("Provide either `selector` or `url`."); 1022 } 1023 if (hasSelector && hasUrl) { 1024 return errorResult("Provide only one of `selector` or `url`, not both."); 1025 } 1026 const result = await inspectImage( 1027 { 1028 ...selector.kind === "ok" ? { selector: selector.value } : {}, 1029 ...url.kind === "ok" ? { url: url.value } : {} 1030 }, 1031 ctx.signal 1032 ); 1033 return ok({ ...result }); 1034}; 1035function wrapErrors(h) { 1036 return async (args, ctx) => { 1037 try { 1038 return await h(args, ctx); 1039 } catch (err) { 1040 return errorResult(err instanceof Error ? err.message : String(err)); 1041 } 1042 }; 1043} 1044var c2paPack = { 1045 kind: "static", 1046 name: "c2pa", 1047 tools: TOOLS, 1048 handlers: { 1049 scan_images_c2pa: wrapErrors(handleScanImages), 1050 inspect_image_c2pa: wrapErrors(handleInspectImage) 1051 } 1052}; 1053
1054// src/bridge/packs/mcp-server-client/mcp-client.ts 1055var MCP_TIMEOUT_MS = 2e4; 1056var MCP_MAX_BYTES = 8 * 1024 * 1024; 1057var MCP_MAX_LIST_PAGES = 20; 1058var RPC_ID = 1; 1059function isObject(v) { 1060 return typeof v === "object" && v !== null && !Array.isArray(v); 1061} 1062async function readBoundedText(res) { 1063 const body = res.body; 1064 if (!body) return res.text(); 1065 const reader = body.getReader(); 1066 const chunks = []; 1067 let total = 0; 1068 try { 1069 for (; ; ) { 1070 const { done, value } = await reader.read(); 1071 if (done) break; 1072 if (value) { 1073 total += value.byteLength; 1074 if (total > MCP_MAX_BYTES) { 1075 throw new Error( 1076 `MCP response exceeded ${MCP_MAX_BYTES} bytes; aborting.` 1077 ); 1078 } 1079 chunks.push(value); 1080 } 1081 } 1082 } finally { 1083 await reader.cancel().catch(() => { 1084 }); 1085 } 1086 const merged = new Uint8Array(total); 1087 let offset = 0; 1088 for (const chunk of chunks) { 1089 merged.set(chunk, offset); 1090 offset += chunk.byteLength; 1091 } 1092 return new TextDecoder().decode(merged); 1093} 1094function callSignal(signal) { 1095 const timeout = AbortSignal.timeout(MCP_TIMEOUT_MS); 1096 return signal ? AbortSignal.any([signal, timeout]) : timeout; 1097} 1098async function mcpRpc(endpoint, method, params, signal) { 1099 const res = await fetch(endpoint, { 1100 method: "POST", 1101 headers: { 1102 "content-type": "application/json", 1103 accept: "application/json, text/event-stream" 1104 }, 1105 // Same-origin endpoint: authenticate as the logged-in user via 1106 // their ambient session cookie. Never forward credentials 1107 // cross-origin. 1108 credentials: "same-origin", 1109 body: JSON.stringify({ jsonrpc: "2.0", id: RPC_ID, method, params }), 1110 signal: callSignal(signal) 1111 }); 1112 if (!res.ok) { 1113 throw new Error(`MCP endpoint returned HTTP ${res.status}`); 1114 } 1115 const contentType = res.headers.get("content-type") ?? ""; 1116 const rpc = contentType.includes("text/event-stream") ? await readEventStreamResponse(res) : parseJson(await readBoundedText(res)); 1117 return extractResult(rpc); 1118} 1119async function mcpListTools(endpoint, signal) { 1120 const out = []; 1121 let cursor; 1122 for (let page = 0; page < MCP_MAX_LIST_PAGES; page++) { 1123 const params = cursor ? { cursor } : {}; 1124 const result = await mcpRpc(endpoint, "tools/list", params, signal); 1125 if (!Array.isArray(result.tools)) { 1126 throw new Error("MCP tools/list response has no tools array"); 1127 } 1128 for (const t of result.tools) { 1129 const def = toToolDef(t); 1130 if (def) out.push(def); 1131 } 1132 cursor = typeof result.nextCursor === "string" && result.nextCursor.length > 0 ? result.nextCursor : void 0; 1133 if (!cursor) break; 1134 } 1135 return out; 1136} 1137function toToolDef(t) { 1138 if (!isObject(t)) return null; 1139 if (typeof t.name !== "string" || t.name.trim().length === 0) return null; 1140 const def = { name: t.name }; 1141 if (typeof t.description === "string" && t.description.trim().length > 0) { 1142 def.description = t.description; 1143 } 1144 if (isObject(t.inputSchema) && t.inputSchema["type"] === "object") { 1145 def.inputSchema = t.inputSchema; 1146 } 1147 return def; 1148} 1149async function mcpCallTool(endpoint, name, args, signal) { 1150 const result = await mcpRpc( 1151 endpoint, 1152 "tools/call", 1153 { name, arguments: args }, 1154 signal 1155 ); 1156 if (!Array.isArray(result.content)) { 1157 throw new Error("MCP tools/call result has no content array"); 1158 } 1159 const out = { 1160 // Drop any structurally-malformed blocks so a misbehaving site tool 1161 // can't push an unstructured payload straight into an agent response. 1162 content: result.content.filter(isContentBlock), 1163 isError: result.isError === true 1164 }; 1165 if (isObject(result.structuredContent)) { 1166 out.structuredContent = result.structuredContent; 1167 } 1168 return out; 1169} 1170function isContentBlock(b) { 1171 if (!isObject(b) || typeof b["type"] !== "string") return false; 1172 if (b["type"] === "text") return typeof b["text"] === "string"; 1173 return true; 1174} 1175function parseJson(raw) { 1176 try { 1177 return JSON.parse(raw); 1178 } catch { 1179 throw new Error("MCP endpoint returned a non-JSON response"); 1180 } 1181} 1182async function readEventStreamResponse(res) { 1183 const body = res.body; 1184 if (!body) return parseEventStream(await res.text()); 1185 const reader = body.getReader(); 1186 const decoder = new TextDecoder(); 1187 let buffer = ""; 1188 let total = 0; 1189 try { 1190 for (; ; ) { 1191 const { done, value } = await reader.read(); 1192 if (done) break; 1193 if (!value) continue; 1194 total += value.byteLength; 1195 if (total > MCP_MAX_BYTES) { 1196 throw new Error(`MCP response exceeded ${MCP_MAX_BYTES} bytes; aborting.`); 1197 } 1198 buffer += decoder.decode(value, { stream: true }); 1199 const lines = buffer.split("\n"); 1200 buffer = lines.pop() ?? ""; 1201 for (const rawLine of lines) { 1202 const env2 = tryParseDataLine(rawLine); 1203 if (env2 !== void 0) return env2; 1204 } 1205 } 1206 const env = tryParseDataLine(buffer); 1207 if (env !== void 0) return env; 1208 } finally { 1209 await reader.cancel().catch(() => { 1210 }); 1211 } 1212 throw new Error("MCP event stream contained no JSON-RPC response"); 1213} 1214function parseEventStream(raw) { 1215 for (const rawLine of raw.split(/\r?\n/)) { 1216 const env = tryParseDataLine(rawLine); 1217 if (env !== void 0) return env; 1218 } 1219 throw new Error("MCP event stream contained no JSON-RPC response"); 1220} 1221function tryParseDataLine(rawLine) { 1222 const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine; 1223 if (!line.startsWith("data:")) return void 0; 1224 const payload = line.slice("data:".length).trim(); 1225 if (!payload) return void 0; 1226 try { 1227 const parsed = JSON.parse(payload); 1228 if (isResponseEnvelope(parsed)) return parsed; 1229 } catch { 1230 } 1231 return void 0; 1232} 1233function isResponseEnvelope(v) { 1234 if (!isObject(v) || v["jsonrpc"] !== "2.0") return false;
1235 const id = v["id"]; 1236 if (id === RPC_ID) return true; 1237 if (id === null || id === void 0) { 1238 return ("result" in v || "error" in v) && !("method" in v); 1239 } 1240 return false; 1241} 1242function extractResult(rpc) { 1243 if (!isObject(rpc)) { 1244 throw new Error("Malformed JSON-RPC response from MCP endpoint"); 1245 } 1246 if ("error" in rpc && rpc.error) { 1247 const err = rpc.error; 1248 const message = isObject(err) && typeof err.message === "string" ? err.message : "unknown error"; 1249 throw new Error(`MCP endpoint error: ${message}`); 1250 } 1251 const result = rpc.result; 1252 if (!isObject(result)) { 1253 throw new Error("MCP response missing result"); 1254 } 1255 return result; 1256} 1257
1258// src/bridge/packs/mcp-server-client/index.ts 1259function isObject2(v) { 1260 return typeof v === "object" && v !== null; 1261} 1262function makeHandler(endpoint, toolName) { 1263 return async (rawArgs, ctx) => { 1264 const args = isObject2(rawArgs) ? rawArgs : {}; 1265 try { 1266 const result = await mcpCallTool(endpoint, toolName, args, ctx.signal); 1267 const out = { 1268 content: result.content 1269 }; 1270 if (isObject2(result.structuredContent)) { 1271 out.structuredContent = result.structuredContent; 1272 } 1273 if (result.isError) out.isError = true; 1274 return customResult(out); 1275 } catch (err) { 1276 return errorResult(err instanceof Error ? err.message : String(err)); 1277 } 1278 }; 1279} 1280async function resolve(ctx) { 1281 const endpoint = ctx.mcpUrl; 1282 if (!endpoint) { 1283 return { tools: [], handlers: {} }; 1284 } 1285 let defs; 1286 try { 1287 defs = await mcpListTools(endpoint, ctx.signal); 1288 } catch (err) { 1289 console.warn( 1290 `[webmcp-interceptor] mcp-server-client: tools/list failed for "${endpoint}"; registering no site tools.`, 1291 err 1292 ); 1293 return { tools: [], handlers: {} }; 1294 } 1295 const tools = []; 1296 const handlers = {}; 1297 for (const def of defs) { 1298 tools.push({ 1299 name: def.name, 1300 // WebMCP requires a non-empty description; fall back to the name 1301 // when it's missing OR empty (`??` alone would preserve an empty 1302 // string, which fails registration). 1303 description: def.description || def.name, 1304 inputSchema: def.inputSchema ?? { 1305 type: "object", 1306 properties: {} 1307 } 1308 }); 1309 handlers[def.name] = makeHandler(endpoint, def.name); 1310 } 1311 console.debug( 1312 `[webmcp-interceptor] mcp-server-client: registered ${tools.length} site tool(s) from ${endpoint}.` 1313 ); 1314 return { tools, handlers }; 1315} 1316var mcpServerClientPack = { 1317 kind: "dynamic", 1318 name: "mcp-server-client", 1319 resolve 1320}; 1321
1322// src/bridge/bridge.ts 1323var ALL_PACKS = [c2paPack, mcpServerClientPack]; 1324var DEFAULT_ACTIVE_PACKS = []; 1325function resolveOwnScript() { 1326 const candidates = Array.from( 1327 document.querySelectorAll("script[src]") 1328 ); 1329 try { 1330 const selfUrl = import.meta.url; 1331 const exact = candidates.find((s) => s.src === selfUrl); 1332 if (exact) return exact; 1333 } catch { 1334 } 1335 return candidates.find((s) => s.src.includes("/.webmcp/bridge.js")) ?? null; 1336} 1337var script = resolveOwnScript(); 1338initBridge().catch((err) => { 1339 console.error("[webmcp-interceptor] Bridge initialisation failed:", err); 1340}); 1341async function initBridge() { 1342 const mc = resolveModelContext(); 1343 if (!mc) { 1344 console.warn( 1345 "[webmcp-interceptor] document.modelContext is not available.\nTo enable WebMCP, use Chrome M146+ and enable:\nchrome://flags/#enable-experimental-web-platform-features" 1346 ); 1347 return; 1348 } 1349 const workerBaseUrl = resolveWorkerBaseUrl(); 1350 const mcpUrl = normalizeMcpUrl(script?.dataset["mcpUrl"]); 1351 const ctx = { 1352 origin: window.location.origin, 1353 workerRpc: workerBaseUrl ? makeWorkerRpc(workerBaseUrl) : workerRpcUnavailable, 1354 ...mcpUrl ? { mcpUrl } : {} 1355 }; 1356 const activePacks = selectActivePacks( 1357 ALL_PACKS, 1358 parsePackList(script?.dataset["packs"]), 1359 DEFAULT_ACTIVE_PACKS 1360 ); 1361 const staticResolved = resolveStaticPacks(activePacks); 1362 const staticRegistry = buildRegistry(staticResolved); 1363 registerTools(mc, ctx, staticRegistry); 1364 const registeredNames = new Set(staticRegistry.tools.map((t) => t.name)); 1365 console.debug( 1366 `[webmcp-interceptor] Registered ${staticRegistry.tools.length} static tool(s); resolving dynamic packs\u2026` 1367 ); 1368 const dynamicResolved = await resolveDynamicPacks(activePacks, ctx); 1369 if (dynamicResolved.length > 0) { 1370 const combined = buildRegistry([...staticResolved, ...dynamicResolved]); 1371 const additions = { 1372 tools: combined.tools.filter((t) => !registeredNames.has(t.name)), 1373 handlers: combined.handlers 1374 }; 1375 registerTools(mc, ctx, additions); 1376 console.debug( 1377 `[webmcp-interceptor] Registered ${additions.tools.length} dynamic tool(s) from ${dynamicResolved.length} dynamic pack(s).` 1378 ); 1379 } 1380} 1381function registerTools(mc, ctx, registry) { 1382 for (const tool of registry.tools) { 1383 const handler = registry.handlers[tool.name]; 1384 if (!handler) continue; 1385 mc.registerTool({ 1386 name: tool.name, 1387 // MCP's `Tool.description` is optional but WebMCP's 1388 // `registerTool()` requires a non-empty string. Fall back to 1389 // the tool name when the description is missing OR empty (`??` 1390 // alone would let an empty string through) so registration never 1391 // fails â the agent at least sees the name. 1392 description: tool.description || tool.name, 1393 inputSchema: tool.inputSchema, 1394 // WebMCP may pass an options bag with an AbortSignal as the 1395 // second argument; forward it so handlers can cancel in-flight 1396 // work. When absent, the per-call context simply has no signal. 1397 execute: (args, options) => invokeHandler(tool.name, handler, args, { 1398 ...ctx, 1399 ...options?.signal ? { signal: options.signal } : {} 1400 }) 1401 }); 1402 } 1403} 1404async function invokeHandler(toolName, handler, args, ctx) { 1405 let result; 1406 try { 1407 result = await handler(args, ctx); 1408 } catch (err) { 1409 const message = err instanceof Error ? err.message : String(err); 1410 console.warn(`[webmcp-interceptor] Tool "${toolName}" threw:`, message); 1411 throw new Error(message); 1412 } 1413 if (result.isError) { 1414 const text = extractText(result) ?? `${toolName} failed`; 1415 const error = new Error(text, { cause: result }); 1416 error.webmcpResult = result; 1417 throw error; 1418 } 1419 if (result.structuredContent !== void 0) return result.structuredContent; 1420 return extractText(result) ?? null; 1421} 1422function extractText(r) { 1423 if (!r.content || r.content.length === 0) return null; 1424 const texts = []; 1425 for (const block of r.content) { 1426 if (block.type === "text") texts.push(block.text); 1427 } 1428 return texts.length === 0 ? null : texts.join("\n"); 1429} 1430function resolveWorkerBaseUrl() { 1431 let src = script?.src; 1432 if (!src) { 1433 try { 1434 src = import.meta.url; 1435 } catch { 1436 src = void 0; 1437 } 1438 } 1439 if (!src) return null; 1440 try { 1441 const url = new URL(src); 1442 const webmcpIndex = url.pathname.indexOf("/.webmcp/"); 1443 if (webmcpIndex >= 0) { 1444 url.pathname = url.pathname.slice(0, webmcpIndex); 1445 } 1446 url.search = ""; 1447 url.hash = ""; 1448 return url.toString().replace(/\/$/, ""); 1449 } catch { 1450 return null; 1451 } 1452} 1453function makeWorkerRpc(baseUrl) { 1454 return async (path, body, validate) => { 1455 const url = `${baseUrl}/.webmcp/rpc/${path.replace(/^\//, "")}`; 1456 const res = await fetch(url, { 1457 method: "POST", 1458 headers: { "content-type": "application/json" }, 1459 // Important: NOT `credentials: 'include'`. The worker is on a 1460 // different origin and we don't want to forward the page's
1461 // cookies to it. Each pack is responsible for sending whatever 1462 // worker-specific auth it needs (today, none). 1463 body: JSON.stringify(body) 1464 }); 1465 const text = await res.text(); 1466 let parsed; 1467 try { 1468 parsed = text === "" ? null : JSON.parse(text); 1469 } catch { 1470 throw new Error(`Worker returned non-JSON response (HTTP ${res.status})`); 1471 } 1472 if (!res.ok) { 1473 const message = parsed && typeof parsed === "object" && "error" in parsed && typeof parsed.error === "string" ? parsed.error : `Worker RPC failed (HTTP ${res.status})`; 1474 throw new Error(message); 1475 } 1476 return validate(parsed); 1477 }; 1478} 1479function workerRpcUnavailable() { 1480 throw new Error( 1481 "Worker base URL could not be determined. Worker-backed tools are unavailable on this page." 1482 ); 1483} 1484function resolveModelContext() { 1485 const fromDocument = document.modelContext; 1486 if (fromDocument) return fromDocument; 1487 const fromNavigator = navigator.modelContext; 1488 return fromNavigator ?? null; 1489}
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.