@ggui-ai/mcp-server 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/api-renders-routes.d.ts.map +1 -1
  2. package/dist/api-renders-routes.js +32 -23
  3. package/dist/api-renders-stream-route.d.ts +80 -0
  4. package/dist/api-renders-stream-route.d.ts.map +1 -0
  5. package/dist/api-renders-stream-route.js +311 -0
  6. package/dist/build-mcp.d.ts +10 -0
  7. package/dist/build-mcp.d.ts.map +1 -1
  8. package/dist/build-mcp.js +64 -4
  9. package/dist/console-session-routes.d.ts.map +1 -1
  10. package/dist/console-session-routes.js +11 -0
  11. package/dist/ggui-session-channel/action-ingress.d.ts +2 -2
  12. package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -1
  13. package/dist/ggui-session-channel/channel-subscriptions.d.ts +4 -4
  14. package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -1
  15. package/dist/ggui-session-channel/internal-types.d.ts +63 -9
  16. package/dist/ggui-session-channel/internal-types.d.ts.map +1 -1
  17. package/dist/ggui-session-channel/outbound.d.ts +15 -5
  18. package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
  19. package/dist/ggui-session-channel/outbound.js +64 -24
  20. package/dist/ggui-session-channel/socket-router.d.ts +8 -3
  21. package/dist/ggui-session-channel/socket-router.d.ts.map +1 -1
  22. package/dist/ggui-session-channel/socket-router.js +6 -1
  23. package/dist/ggui-session-channel/subscribe.d.ts +48 -3
  24. package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
  25. package/dist/ggui-session-channel/subscribe.js +97 -36
  26. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +23 -13
  27. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -1
  28. package/dist/ggui-session-channel/subscriber-lifecycle.js +24 -11
  29. package/dist/ggui-session-channel.d.ts +58 -11
  30. package/dist/ggui-session-channel.d.ts.map +1 -1
  31. package/dist/ggui-session-channel.js +55 -19
  32. package/dist/index.d.ts +4 -3
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +7 -1
  35. package/dist/instructions-presets.js +10 -10
  36. package/dist/mcp-apps-outbound.d.ts +49 -4
  37. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  38. package/dist/mcp-apps-outbound.js +359 -63
  39. package/dist/mcp-endpoint-routes.d.ts +17 -5
  40. package/dist/mcp-endpoint-routes.d.ts.map +1 -1
  41. package/dist/mcp-endpoint-routes.js +2 -0
  42. package/dist/oauth-as-routes.d.ts.map +1 -1
  43. package/dist/oauth-as-routes.js +36 -0
  44. package/dist/oauth.d.ts.map +1 -1
  45. package/dist/oauth.js +8 -1
  46. package/dist/runtime-bundle-hash.d.ts +43 -0
  47. package/dist/runtime-bundle-hash.d.ts.map +1 -0
  48. package/dist/runtime-bundle-hash.js +66 -0
  49. package/dist/runtime-bundle-route.js +1 -1
  50. package/dist/server.d.ts +16 -5
  51. package/dist/server.d.ts.map +1 -1
  52. package/dist/server.js +93 -19
  53. package/package.json +12 -12
@@ -27,13 +27,13 @@
27
27
  * The thin shell is static content; it depends on nothing except the
28
28
  * MIME constant and the HTML. Keeping it next to the registration
29
29
  * means a future refactor of the shell edits one file. The
30
- * `@ggui-ai/react` package does NOT ship the shell as a separate
30
+ * `@ggui-ai/mcp-apps-react` package does NOT ship the shell as a separate
31
31
  * build target — per the design lock, the shell is served by the
32
32
  * same `@ggui-ai/mcp-server` instance that mints the bootstrap.
33
33
  */
34
- import { deriveBundleOrigins, deriveContractBundle, derivePublicEnvProjection, deriveRenderMeta, filterDescriptorsToContract, findBlueprintExact, } from "@ggui-ai/mcp-server-handlers/renders";
34
+ import { deriveBundleOrigins, deriveContractBundle, derivePublicEnvProjection, deriveRenderMeta, filterDescriptorsToContract, findBlueprintExact, spreadRenderMetaViewOntoSlice, wsOriginToHttpOrigin, } from "@ggui-ai/mcp-server-handlers/renders";
35
35
  import { RESOURCE_NOT_FOUND_MESSAGE, deriveContextDefault, isRecord, resolveAppGadgets, resourceReadErrorToJsonRpc, } from "@ggui-ai/protocol";
36
- import { GGUI_RENDER_RESOURCE_MIME, GGUI_RENDER_RESOURCE_URI, GGUI_RENDER_SHELL_SURFACE, MCP_APPS_UI_CAPABILITY, MCP_APP_BOOTSTRAP_FAILED_TYPE, asGguiRenderBootstrap, deriveContextName, escapeInlineScript, gguiShellHtml, toMcpAppEnvelope, } from "@ggui-ai/protocol/integrations/mcp-apps";
36
+ import { GGUI_RENDER_RESOURCE_MIME, GGUI_RENDER_RESOURCE_URI, GGUI_RENDER_SHELL_SURFACE, MCP_APPS_UI_CAPABILITY, MCP_APP_BOOTSTRAP_FAILED_TYPE, asGguiRenderBootstrap, composeSessionApiUrls, deriveContextName, escapeInlineScript, gguiShellHtml, parseEpochUri, toMcpAppEnvelope, } from "@ggui-ai/protocol/integrations/mcp-apps";
37
37
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
38
38
  import { registerAppResource } from "@modelcontextprotocol/ext-apps/server";
39
39
  import { createHash } from "node:crypto";
@@ -150,9 +150,39 @@ var rpcId=1,pending={};
150
150
  var rootEl=document.getElementById('ggui-root');
151
151
  rootEl.style.cssText='display:flex;flex-direction:column;height:100%;min-height:300px;margin:0';
152
152
  var mounted=false;
153
+ var lastEnvelope=null;
154
+ // Text color pairs with the shell surface: themed var when the runtime
155
+ // injected theme CSS, else the light-on-dark fallback matching the
156
+ // shell's static #1e293b pre-theme surface. The old hardcoded #666 was
157
+ // illegible on that dark fallback (#481).
158
+ var SHELL_FG='var(--ggui-color-onSurface,#e2e8f0)';
153
159
  function setOverlay(text){
154
160
  if(mounted)return;
155
- rootEl.innerHTML='<div style="font:14px system-ui,sans-serif;padding:24px;color:#666">'+text+'</div>';
161
+ rootEl.innerHTML='<div style="font:13px system-ui,sans-serif;padding:24px;color:'+SHELL_FG+';opacity:.55">'+text+'</div>';
162
+ }
163
+ function escText(s){
164
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
165
+ }
166
+ // Terminal failure card (#481): plain-language summary, diagnostic
167
+ // reachable but collapsed, Retry only when a retry can actually work.
168
+ // Same visual vocabulary as the runtime's React error boundary so the
169
+ // two error surfaces read as one system.
170
+ function showFailure(summary,detail,retry){
171
+ if(mounted)return;
172
+ rootEl.innerHTML=''+
173
+ '<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:32px 24px;min-height:160px;font-family:system-ui,sans-serif;color:'+SHELL_FG+';text-align:center">'+
174
+ '<div style="font-size:14px;font-weight:600">'+escText(summary)+'</div>'+
175
+ '<div style="font-size:12px;opacity:.65;max-width:320px;line-height:1.45">The interface could not start. The conversation is unaffected — you can also just ask for the view again.</div>'+
176
+ (retry?'<button id="ggui-shell-retry" style="margin-top:2px;padding:7px 18px;border-radius:8px;border:1px solid currentColor;background:transparent;color:inherit;opacity:.75;font:500 13px system-ui,sans-serif;cursor:pointer">Retry</button>':'')+
177
+ '<details style="margin-top:6px;max-width:340px;width:100%;text-align:left;opacity:.75">'+
178
+ '<summary style="cursor:pointer;font-size:12px">Details</summary>'+
179
+ '<pre style="margin:6px 0 0;font:11px/1.5 ui-monospace,Menlo,monospace;white-space:pre-wrap;word-break:break-word">'+escText(detail)+'</pre>'+
180
+ '</details>'+
181
+ '</div>';
182
+ if(retry){
183
+ var b=document.getElementById('ggui-shell-retry');
184
+ if(b)b.onclick=retry;
185
+ }
156
186
  }
157
187
  function postNotification(method,params){
158
188
  try{window.parent.postMessage({jsonrpc:'2.0',method:method,params:params||{}},'*');}catch(e){}
@@ -183,10 +213,12 @@ async function mountFromMeta(envelope){
183
213
  var renderSlice=envelope&&envelope['ai.ggui/render'];
184
214
  var runtimeUrl=renderSlice&&renderSlice.runtimeUrl;
185
215
  if(!envelope||typeof runtimeUrl!=='string'){
186
- setOverlay('Bootstrap payload malformed.');
216
+ // No retry: without a valid envelope there is nothing to re-run.
217
+ showFailure('This view could not start','MALFORMED_BOOTSTRAP: bootstrap payload malformed (no ai.ggui/render slice with a runtimeUrl).',null);
187
218
  postBootstrapFailed('MALFORMED_BOOTSTRAP','Bootstrap payload malformed.');
188
219
  return;
189
220
  }
221
+ lastEnvelope=envelope;
190
222
  setOverlay('Loading UI…');
191
223
  window.__GGUI_META__=envelope;
192
224
  // Load the runtime bundle via a direct cross-origin script tag
@@ -212,14 +244,14 @@ async function mountFromMeta(envelope){
212
244
  s.onload=function(){mounted=true;};
213
245
  s.onerror=function(e){
214
246
  var msg='Runtime bundle failed to load: '+(e&&e.message||'script error');
215
- setOverlay(msg);
247
+ showFailure('This view could not load','BUNDLE_FETCH_FAILED: '+msg,function(){mountFromMeta(lastEnvelope);});
216
248
  postBootstrapFailed('BUNDLE_FETCH_FAILED',msg);
217
249
  };
218
250
  rootEl.innerHTML='';
219
251
  document.body.appendChild(s);
220
252
  }catch(e){
221
253
  var msg='Runtime bundle failed to load: '+(e&&e.message||e);
222
- setOverlay(msg);
254
+ showFailure('This view could not load','BUNDLE_FETCH_FAILED: '+msg,function(){mountFromMeta(lastEnvelope);});
223
255
  postBootstrapFailed('BUNDLE_FETCH_FAILED',msg);
224
256
  }
225
257
  }
@@ -262,26 +294,32 @@ window.addEventListener('message',function(ev){
262
294
  // above.
263
295
  }
264
296
  });
265
- setOverlay('Initializing…');
266
- var initTimer=setTimeout(function(){
267
- setOverlay('Host did not respond to ui/initialize within 3s.');
268
- },3000);
269
- postRpc('ui/initialize',{
270
- appCapabilities:{},
271
- appInfo:{name:'ggui-render',version:'1.0.0'},
272
- protocolVersion:'2026-01-26'
273
- }).then(function(){
274
- clearTimeout(initTimer);
275
- postNotification('ui/notifications/initialized',{});
276
- // Wait for the host to send ui/notifications/tool-result carrying
277
- // the slice envelope in _meta — the spec-canonical delivery channel.
278
- // The ui/initialize result itself carries no slice meta (the
279
- // McpUiInitializeResult schema defines no such field).
280
- setOverlay('Waiting for tool result…');
281
- }).catch(function(e){
282
- clearTimeout(initTimer);
283
- setOverlay('ui/initialize failed: '+(e&&e.message||JSON.stringify(e)));
284
- });
297
+ // Named so the failure card's Retry can re-run the whole handshake —
298
+ // a host that missed or rejected the first ui/initialize may answer a
299
+ // second (observed with hosts that attach their listener late).
300
+ function startInit(){
301
+ setOverlay('Initializing…');
302
+ var initTimer=setTimeout(function(){
303
+ showFailure('This view did not hear back from its host','INIT_TIMEOUT: no response to ui/initialize within 3s.',startInit);
304
+ },3000);
305
+ postRpc('ui/initialize',{
306
+ appCapabilities:{},
307
+ appInfo:{name:'ggui-render',version:'1.0.0'},
308
+ protocolVersion:'2026-01-26'
309
+ }).then(function(){
310
+ clearTimeout(initTimer);
311
+ postNotification('ui/notifications/initialized',{});
312
+ // Wait for the host to send ui/notifications/tool-result carrying
313
+ // the slice envelope in _meta — the spec-canonical delivery channel.
314
+ // The ui/initialize result itself carries no slice meta (the
315
+ // McpUiInitializeResult schema defines no such field).
316
+ setOverlay('Waiting for tool result…');
317
+ }).catch(function(e){
318
+ clearTimeout(initTimer);
319
+ showFailure('This view could not connect to its host','ui/initialize failed: '+(e&&e.message||JSON.stringify(e)),startInit);
320
+ });
321
+ }
322
+ startInit();
285
323
  })();
286
324
  `;
287
325
  // `--ggui-color-surface` is injected at `:root` on this document's
@@ -431,9 +469,16 @@ try{
431
469
  * deliberately untouched.
432
470
  */
433
471
  export function buildInlineRenderShellHtml(runtimeSource) {
472
+ // No anchor div: the runtime appends its own mount target to
473
+ // `document.body` at boot, so a thin-shell-style `#ggui-root`
474
+ // placeholder here would just stack empty space ABOVE the rendered
475
+ // card (min-height'd blank div + content below it — the "long upper
476
+ // space" bug from the first claude.ai live test). Same
477
+ // no-container posture as `gguiShellHtml`; the shell marker rides
478
+ // on `<body>`.
434
479
  return `<!doctype html>
435
- <html lang="en" style="height:100%;background-color:${GGUI_RENDER_SHELL_SURFACE}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="light dark"><title>ggui render</title></head>
436
- <body style="margin:0;height:100%;min-height:480px;background-color:${GGUI_RENDER_SHELL_SURFACE}"><div id="ggui-root" data-ggui-shell="inline" style="height:100%;min-height:480px"></div>
480
+ <html lang="en" style="background-color:${GGUI_RENDER_SHELL_SURFACE}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="light dark"><title>ggui render</title></head>
481
+ <body style="margin:0;background-color:${GGUI_RENDER_SHELL_SURFACE}" data-ggui-shell="inline">
437
482
  <script>${GGUI_INLINE_SHELL_BUFFER_SCRIPT_BODY}</script>
438
483
  <script type="module" data-ggui-runtime="inline">${escapeInlineScript(runtimeSource)}</script></body></html>`;
439
484
  }
@@ -483,7 +528,17 @@ function buildCspMeta(publicBaseUrl,
483
528
  * references `:6786/_ggui/iframe-runtime.js`) trips a `script-src`
484
529
  * violation that blanks the iframe — verified live 2026-05-27.
485
530
  */
486
- runtimeUrl) {
531
+ runtimeUrl,
532
+ /**
533
+ * Origins of server-stamped fetch/stream URLs (`sseUrl`,
534
+ * `pollingUrl`) — each parseable entry's origin is unioned into
535
+ * `connectDomains` (deduplicated). Same-origin deployments add
536
+ * nothing new; split-origin session APIs (dedicated streaming host)
537
+ * would otherwise be silently blocked by `connect-src` on
538
+ * spec-compliant hosts — EventSource and fetch are both
539
+ * connect-src-governed.
540
+ */
541
+ extraConnectUrls) {
487
542
  const source = publicBaseUrl ?? runtimeUrl;
488
543
  if (!source)
489
544
  return undefined;
@@ -492,10 +547,26 @@ runtimeUrl) {
492
547
  const origin = parsed.origin;
493
548
  const wsScheme = parsed.protocol === "https:" ? "wss:" : "ws:";
494
549
  const wsOrigin = `${wsScheme}//${parsed.host}`;
550
+ const connectDomains = [origin, wsOrigin];
551
+ for (const url of extraConnectUrls ?? []) {
552
+ if (url === undefined)
553
+ continue;
554
+ let extraOrigin;
555
+ try {
556
+ extraOrigin = new URL(url).origin;
557
+ }
558
+ catch {
559
+ // Unparseable stamped URL — nothing to declare for it; the
560
+ // base declaration stands.
561
+ continue;
562
+ }
563
+ if (!connectDomains.includes(extraOrigin))
564
+ connectDomains.push(extraOrigin);
565
+ }
495
566
  return {
496
567
  ui: {
497
568
  csp: {
498
- connectDomains: [origin, wsOrigin],
569
+ connectDomains,
499
570
  resourceDomains: [origin],
500
571
  },
501
572
  },
@@ -518,21 +589,53 @@ export function registerGguiRenderResource(server, shellHtml = GGUI_RENDER_SHELL
518
589
  * declaration at all and spec-compliant hosts applied the
519
590
  * restrictive default (`connect-src 'none'`).
520
591
  */
521
- runtimeUrl) {
522
- const cspMeta = buildCspMeta(publicBaseUrl, runtimeUrl);
592
+ runtimeUrl,
593
+ /**
594
+ * Additional URLs whose origins the mounted iframe must be able to
595
+ * `connect-src` — unioned into `connectDomains` by
596
+ * {@link buildCspMeta}. The load-bearing entries are the live-channel
597
+ * origins (`wsUrl` + its ws→http origin flip): the STATIC shell is
598
+ * the resource cross-origin hosts (claude.ai) mount and derive the
599
+ * frame CSP from, so origins declared only on per-render resources
600
+ * never reach the frame. Without these, deployments that set no
601
+ * `publicBaseUrl` (the cloud pod — it feeds Origin/Host enforcement)
602
+ * declare only the runtime-CDN origin and every SSE / HTTP-polling /
603
+ * WS rung of the failover ladder is CSP-blocked in the mounted
604
+ * iframe — observed live on claude.ai (#471 round 11: frame booted
605
+ * with `connect-src assets.mcp.ggui.ai` only).
606
+ */
607
+ extraConnectUrls) {
608
+ const cspMeta = buildCspMeta(publicBaseUrl, runtimeUrl, extraConnectUrls);
609
+ // Content-addressed shell URI (2026-08-12, the stale-shell bust).
610
+ // Hosts cache the prefetched shell keyed on the RESOURCE URI —
611
+ // claude.ai's backend was observed serving days-old shell bytes
612
+ // across our deploys, fresh pages, and connector re-connects,
613
+ // because `ui://ggui/render` never changes. Hash the FULL SERVED
614
+ // REPRESENTATION — shell bytes (wrapper + any inlined runtime) AND
615
+ // the `_meta.ui.csp` declaration — into the advertised URI so any
616
+ // change a host may have cached mints a NEW URI, and an unchanged
617
+ // one never does; the same content-address discipline the hashed
618
+ // `/_ggui/iframe-runtime.<sha12>.js` HTTP route applies one layer
619
+ // down. The meta MUST be in the hash input: hosts cache the
620
+ // declaration alongside the bytes (claude.ai derives the frame's
621
+ // connect-src from it), so a meta-only change — e.g. adding the
622
+ // live-channel origins to `connectDomains` — would otherwise ship a
623
+ // new policy under an old URI and never reach cached frames. The
624
+ // bare URI stays registered for grandfathered sessions and hosts
625
+ // that read it directly.
626
+ const shellHash = createHash("sha256")
627
+ .update(shellHtml)
628
+ .update(JSON.stringify(cspMeta ?? null))
629
+ .digest("hex")
630
+ .slice(0, 12);
631
+ const versionedUri = `${GGUI_RENDER_RESOURCE_URI}/rt-${shellHash}`;
523
632
  // `registerAppResource` (from `@modelcontextprotocol/ext-apps/server`)
524
633
  // defaults `mimeType` to `RESOURCE_MIME_TYPE` — the same
525
634
  // `text/html;profile=mcp-app` value `GGUI_RENDER_RESOURCE_MIME`
526
635
  // carries. Letting the canonical helper own the default means the
527
636
  // mimeType string lives in ONE place across the ecosystem (the SDK)
528
637
  // rather than duplicated in our protocol package.
529
- registerAppResource(server, "ggui-render", GGUI_RENDER_RESOURCE_URI, {
530
- // `title` / `description` show up in MCP clients that surface
531
- // resource metadata. Short + concrete.
532
- title: "ggui render",
533
- description: "Thin-shell iframe bundle that bootstraps a ggui render. MCP Apps hosts fetch this when they see `_meta.ui.resourceUri` on a ggui_render result.",
534
- mimeType: GGUI_RENDER_RESOURCE_MIME,
535
- }, async (uri) => ({
638
+ const serveShell = async (uri) => ({
536
639
  contents: [
537
640
  {
538
641
  uri: uri.href,
@@ -541,7 +644,44 @@ runtimeUrl) {
541
644
  ...(cspMeta !== undefined ? { _meta: cspMeta } : {}),
542
645
  },
543
646
  ],
544
- }));
647
+ });
648
+ registerAppResource(server, "ggui-render", GGUI_RENDER_RESOURCE_URI, {
649
+ // `title` / `description` show up in MCP clients that surface
650
+ // resource metadata. Short + concrete.
651
+ title: "ggui render",
652
+ description: "Thin-shell iframe bundle that bootstraps a ggui render. MCP Apps hosts fetch this when they see `_meta.ui.resourceUri` on a ggui_render result.",
653
+ mimeType: GGUI_RENDER_RESOURCE_MIME,
654
+ }, serveShell);
655
+ registerAppResource(server, "ggui-render-versioned", versionedUri, {
656
+ title: "ggui render (content-addressed)",
657
+ description: "Content-addressed twin of ui://ggui/render — the URI embeds the shell-bytes hash so host prefetch caches miss exactly when the shell changed. Tool declarations advertise THIS URI.",
658
+ mimeType: GGUI_RENDER_RESOURCE_MIME,
659
+ }, serveShell);
660
+ // STALE-HASH grandfather template — `rt-{shellHash}` for ANY hash.
661
+ // Hosts snapshot tool declarations (claude.ai stores the connector's
662
+ // tool list server-side), so after a shell-changing deploy they keep
663
+ // asking for the PREVIOUS deploy's versioned URI. Without this
664
+ // template that read falls through to the per-session
665
+ // `ui://ggui/render/{sessionId}` template (a bare `rt-abc…` segment
666
+ // parses as a sessionId), resolves no render, and the host shows
667
+ // "unable to reach" — observed live on claude.ai the first deploy
668
+ // after the URI scheme changed (#471 round 12). Serving the CURRENT
669
+ // shell under the stale URI restores the pre-content-addressing
670
+ // behavior for stale hosts (at worst they cache today's shell under
671
+ // yesterday's key) while fresh declarations keep the cache-bust
672
+ // property. MUST register before the session templates
673
+ // (`installMcpAppsOutbound` orders this call first; the SDK matches
674
+ // templates in registration order).
675
+ server.registerResource("ggui-render-versioned-grandfather", new ResourceTemplate(`${GGUI_RENDER_RESOURCE_URI}/rt-{shellHash}`, {
676
+ // No list-callback — same posture as the session templates; the
677
+ // canonical URI is the one advertised on tool declarations.
678
+ list: undefined,
679
+ }), {
680
+ title: "ggui render (content-addressed, any revision)",
681
+ description: "Grandfather route for content-addressed shell URIs from earlier deploys — hosts holding a stale tool-declaration snapshot read their old rt-<hash> URI and receive the CURRENT shell.",
682
+ mimeType: GGUI_RENDER_RESOURCE_MIME,
683
+ }, serveShell);
684
+ return versionedUri;
545
685
  }
546
686
  /**
547
687
  * Advertise the `io.modelcontextprotocol/ui` extension capability on
@@ -650,7 +790,11 @@ export function buildSelfContainedShell(opts) {
650
790
  // WS-only mode (legacy behavior). See SelfContainedShellInputs
651
791
  // .pollingUrl for the URL shape.
652
792
  ...(opts.pollingUrl !== undefined ? { pollingUrl: opts.pollingUrl } : {}),
793
+ // SSE middle rung — same stamping posture as pollingUrl. See
794
+ // SelfContainedShellInputs.sseUrl for the stream contract.
795
+ ...(opts.sseUrl !== undefined ? { sseUrl: opts.sseUrl } : {}),
653
796
  ...(opts.lastSequence !== undefined ? { lastSequence: opts.lastSequence } : {}),
797
+ ...(opts.epoch !== undefined ? { epoch: opts.epoch } : {}),
654
798
  // Visible-bits surface — what the iframe is mounting right now.
655
799
  // Static-content discriminators (codeUrl / kind) are mutually
656
800
  // exclusive; the iframe-runtime rejects the both-set mix.
@@ -959,18 +1103,24 @@ export function registerGguiRenderResourceTemplate(server, opts) {
959
1103
  * claude.ai's iframe CSP and the component fails to render. Returns
960
1104
  * `undefined` when there's no base CSP at all (publicBaseUrl
961
1105
  * absent — first-party same-origin host).
1106
+ *
1107
+ * `base` defaults to the registration-time `templateCspMeta`;
1108
+ * `serveMount` passes a per-render base recomputed with the stamped
1109
+ * session-API URLs (`sseUrl` / `pollingUrl`) so their origins ride
1110
+ * `connectDomains` even when they differ from the publicBaseUrl
1111
+ * origin (ws→http origin-flip fallback).
962
1112
  */
963
- const augmentCspMeta = (gadgetOrigins) => {
964
- if (templateCspMeta === undefined)
1113
+ const augmentCspMeta = (gadgetOrigins, base = templateCspMeta) => {
1114
+ if (base === undefined)
965
1115
  return undefined;
966
1116
  if (gadgetOrigins === undefined)
967
- return templateCspMeta;
1117
+ return base;
968
1118
  return {
969
1119
  ui: {
970
1120
  csp: {
971
- connectDomains: [...templateCspMeta.ui.csp.connectDomains, ...gadgetOrigins.connect],
1121
+ connectDomains: [...base.ui.csp.connectDomains, ...gadgetOrigins.connect],
972
1122
  resourceDomains: [
973
- ...templateCspMeta.ui.csp.resourceDomains,
1123
+ ...base.ui.csp.resourceDomains,
974
1124
  ...gadgetOrigins.script,
975
1125
  ...gadgetOrigins.style,
976
1126
  ],
@@ -1476,6 +1626,20 @@ export function registerGguiRenderResourceTemplate(server, opts) {
1476
1626
  channelFault ??= { cause };
1477
1627
  }
1478
1628
  }
1629
+ // Token-bearing session-API URL pair (pollingUrl + sseUrl) —
1630
+ // composed via the protocol's ONE composer so this surface cannot
1631
+ // drift from the render/update resultMeta stamping. Stamped only
1632
+ // when the mint above produced a token (both URLs embed it); base
1633
+ // = publicBaseUrl when configured, else the ws→http origin flip
1634
+ // of the minted wsUrl (session API served on the WS origin — OSS
1635
+ // defaults + the cloud pod's single ingress).
1636
+ let sessionApiUrls;
1637
+ if (wsToken !== undefined) {
1638
+ const base = opts.publicBaseUrl ?? (wsUrl !== undefined ? wsOriginToHttpOrigin(wsUrl) : undefined);
1639
+ if (base !== undefined) {
1640
+ sessionApiUrls = composeSessionApiUrls(base, sessionId, wsToken);
1641
+ }
1642
+ }
1479
1643
  // Mount-mode gate (below the live-channel mint): a compiled
1480
1644
  // component needs ONE of the two channels. A deployment that wires
1481
1645
  // no codeStore (codeUrl === undefined) but DOES wire mintWsToken
@@ -1532,24 +1696,23 @@ export function registerGguiRenderResourceTemplate(server, opts) {
1532
1696
  : {}),
1533
1697
  ...(opts.themeId !== undefined ? { themeId: opts.themeId } : {}),
1534
1698
  ...(opts.themeMode !== undefined ? { themeMode: opts.themeMode } : {}),
1535
- // Per-app theme overlay projected by `deriveRenderMeta` from
1536
- // the render's `theme` sidecar — forwarded so the
1537
- // resource-served iframe matches the postMessage path.
1538
- ...(view.theme !== undefined ? { theme: view.theme } : {}),
1539
- ...(view.propsJson !== undefined ? { propsJson: view.propsJson } : {}),
1540
- ...(view.contextSlots !== undefined ? { contextSlots: view.contextSlots } : {}),
1541
- ...(view.permissionsPolicy !== undefined
1542
- ? { permissionsPolicy: view.permissionsPolicy }
1543
- : {}),
1544
- ...(view.gadgets !== undefined && view.gadgets.length > 0
1545
- ? { gadgets: view.gadgets }
1546
- : {}),
1699
+ // State + policy view fields (theme overlay, propsJson,
1700
+ // contextSlots, permissionsPolicy, gadgets, #483 epoch) — ONE
1701
+ // shared spread so the resource-served shell cannot drift from
1702
+ // the postMessage-path emitters.
1703
+ ...spreadRenderMetaViewOntoSlice(view),
1547
1704
  ...(contractHash !== undefined && validatorsUrl !== undefined
1548
1705
  ? { contractHash, validatorsUrl }
1549
1706
  : {}),
1550
1707
  ...(resourcePublicEnv !== undefined && Object.keys(resourcePublicEnv).length > 0
1551
1708
  ? { publicEnv: resourcePublicEnv }
1552
1709
  : {}),
1710
+ // Token-bearing HTTP fallback rungs — present exactly when the
1711
+ // mint above produced a token and a base resolved (see the
1712
+ // composition above the mount-mode gate).
1713
+ ...(sessionApiUrls !== undefined
1714
+ ? { pollingUrl: sessionApiUrls.pollingUrl, sseUrl: sessionApiUrls.sseUrl }
1715
+ : {}),
1553
1716
  // R6 — ledger cursor stamp for polling-cursor alignment.
1554
1717
  lastSequence: accessibleStored.eventSequence,
1555
1718
  });
@@ -1563,21 +1726,121 @@ export function registerGguiRenderResourceTemplate(server, opts) {
1563
1726
  // derives these via deriveBundleOrigins; this is the per-call
1564
1727
  // resource mirror.
1565
1728
  const gadgetOrigins = deriveBundleOrigins(picked.source);
1566
- return shellContents(uri, html, augmentCspMeta(gadgetOrigins));
1729
+ // Per-render CSP base: recompute with the stamped session-API
1730
+ // URLs so their origins ride `connectDomains` (EventSource +
1731
+ // fetch are connect-src-governed). Same-origin stamps dedupe to
1732
+ // the registration-time declaration; a base derived from the
1733
+ // wsUrl origin flip (publicBaseUrl absent) adds the flip origin
1734
+ // that would otherwise be silently blocked.
1735
+ const renderCspBase = sessionApiUrls !== undefined
1736
+ ? buildCspMeta(opts.publicBaseUrl, opts.runtimeUrl, [
1737
+ sessionApiUrls.sseUrl,
1738
+ sessionApiUrls.pollingUrl,
1739
+ ])
1740
+ : templateCspMeta;
1741
+ return shellContents(uri, html, augmentCspMeta(gadgetOrigins, renderCspBase));
1742
+ }
1743
+ /**
1744
+ * Reconstruct the props of history record `#epoch` from the event
1745
+ * ledger (#483): walk ascending, apply every `ui.updated`, and stop
1746
+ * at the `ui.reminted` boundary that LEAVES the pinned epoch
1747
+ * (`data.epoch === epoch + 1`) — so the record includes the amends
1748
+ * made during its reign, matching the live freeze semantics
1749
+ * (state-at-supersession). Returns `null` when the walk cannot reach
1750
+ * the boundary (ledger horizon evicted the record's reign) — the
1751
+ * caller surfaces the standard not-found posture; the record aged
1752
+ * out of what this server can serve.
1753
+ */
1754
+ async function reconstructPropsAtEpoch(sessionId, epoch) {
1755
+ let since = 0;
1756
+ let currentProps = null;
1757
+ for (;;) {
1758
+ const page = await opts.renderStore.listEventsSince(sessionId, since, 200);
1759
+ if (page === null || page.events.length === 0)
1760
+ return null;
1761
+ for (const event of page.events) {
1762
+ if (event.type === "ui.updated") {
1763
+ const data = event.data;
1764
+ // Epoch-stamped filtering: the update that MINTS epoch N+1
1765
+ // appends its props event (stamped N+1) BEFORE the N+1
1766
+ // boundary — those props belong to the NEXT record, never
1767
+ // to #N. Pre-#483 events carry no stamp and read as
1768
+ // belonging to the then-current (≤ pinned) epoch.
1769
+ if ((data.epoch ?? 0) <= epoch) {
1770
+ currentProps = data.props;
1771
+ }
1772
+ }
1773
+ else if (event.type === "ui.reminted") {
1774
+ const data = event.data;
1775
+ if (data.epoch === epoch + 1)
1776
+ return currentProps;
1777
+ }
1778
+ since = event.seq;
1779
+ }
1780
+ if (!page.hasMore && page.lastSequence <= since)
1781
+ return null;
1782
+ }
1567
1783
  }
1568
1784
  // Single shared handler powers both templates. `blueprintKey` is
1569
1785
  // optional in the variables map — present for the resume URI shape,
1570
1786
  // absent for the legacy single-segment shape.
1571
1787
  async function handle(uri, variables) {
1572
1788
  const sessionIdRaw = variables["sessionId"];
1573
- const sessionId = Array.isArray(sessionIdRaw) ? sessionIdRaw[0] : sessionIdRaw;
1789
+ let sessionId = Array.isArray(sessionIdRaw) ? sessionIdRaw[0] : sessionIdRaw;
1790
+ const blueprintKeyRaw = variables["blueprintKey"];
1791
+ let blueprintKey = Array.isArray(blueprintKeyRaw) ? blueprintKeyRaw[0] : blueprintKeyRaw;
1792
+ // Epoch pin (#483): `…#N` names the immutable history record N;
1793
+ // bare names the live head. Depending on the transport's URL
1794
+ // handling the pin may arrive as `uri.hash`, glued RAW onto the
1795
+ // last matched variable, or PERCENT-ENCODED inside it (`%23N`) —
1796
+ // resolve all three tolerantly via the one seam, cleaning the
1797
+ // variable either way.
1798
+ const parsePin = (segment) => {
1799
+ const direct = parseEpochUri(segment);
1800
+ if (direct.epoch !== undefined) {
1801
+ return { base: direct.baseUri, epoch: direct.epoch };
1802
+ }
1803
+ try {
1804
+ const decoded = decodeURIComponent(segment);
1805
+ if (decoded !== segment) {
1806
+ const parsed = parseEpochUri(decoded);
1807
+ if (parsed.epoch !== undefined) {
1808
+ return { base: parsed.baseUri, epoch: parsed.epoch };
1809
+ }
1810
+ }
1811
+ }
1812
+ catch {
1813
+ // Malformed percent-encoding — not a pin; segment passes
1814
+ // through whole (same tolerant posture as parseEpochUri).
1815
+ }
1816
+ return { base: segment };
1817
+ };
1818
+ // ALWAYS clean the variables (transports have been observed to
1819
+ // deliver the pin BOTH as uri.hash and glued raw onto the matched
1820
+ // variable); the pin resolves from whichever source carried it.
1821
+ let pinnedEpoch;
1822
+ if (uri.hash.length > 1) {
1823
+ pinnedEpoch = parseEpochUri(`x${uri.hash}`).epoch;
1824
+ }
1825
+ if (typeof blueprintKey === "string") {
1826
+ const parsed = parsePin(blueprintKey);
1827
+ if (parsed.epoch !== undefined) {
1828
+ pinnedEpoch = pinnedEpoch ?? parsed.epoch;
1829
+ blueprintKey = parsed.base;
1830
+ }
1831
+ }
1832
+ if (typeof sessionId === "string") {
1833
+ const parsed = parsePin(sessionId);
1834
+ if (parsed.epoch !== undefined) {
1835
+ pinnedEpoch = pinnedEpoch ?? parsed.epoch;
1836
+ sessionId = parsed.base;
1837
+ }
1838
+ }
1574
1839
  if (typeof sessionId !== "string" || sessionId.length === 0) {
1575
1840
  // A URI with no session segment names no locator, which is the
1576
1841
  // same thing as naming one that does not exist.
1577
1842
  throw new ResourceReadFailure(NOT_FOUND_FAILURE);
1578
1843
  }
1579
- const blueprintKeyRaw = variables["blueprintKey"];
1580
- const blueprintKey = Array.isArray(blueprintKeyRaw) ? blueprintKeyRaw[0] : blueprintKeyRaw;
1581
1844
  const hasResumeKey = typeof blueprintKey === "string" && blueprintKey.length > 0;
1582
1845
  // The failure this read ends in if nothing mounts. Seeded from a
1583
1846
  // property of the SERVER, never of the locator, so a caller cannot
@@ -1610,6 +1873,38 @@ export function registerGguiRenderResourceTemplate(server, opts) {
1610
1873
  })
1611
1874
  ? stored
1612
1875
  : null;
1876
+ // Pinned history read (#483): `#N` where N is a SUPERSEDED epoch
1877
+ // reconstructs that record's props from the ledger and serves a
1878
+ // shell frozen at them. N === head falls through to the live
1879
+ // mount (the pinned URI of the current head IS the head); N >
1880
+ // head names a record that does not exist.
1881
+ if (accessibleStored && pinnedEpoch !== undefined) {
1882
+ const headEpoch = accessibleStored.render.epoch ?? 0;
1883
+ if (pinnedEpoch > headEpoch) {
1884
+ throw new ResourceReadFailure(NOT_FOUND_FAILURE);
1885
+ }
1886
+ if (pinnedEpoch < headEpoch && accessibleStored.render.type !== "mcpApps") {
1887
+ const historicalProps = await reconstructPropsAtEpoch(sessionId, pinnedEpoch);
1888
+ if (historicalProps === null) {
1889
+ // The record's reign aged out of the ledger horizon — this
1890
+ // server can no longer serve it. Same terminal posture as a
1891
+ // locator that never existed (see #483 SPEC note).
1892
+ throw new ResourceReadFailure(failure);
1893
+ }
1894
+ const pinnedRow = {
1895
+ ...accessibleStored,
1896
+ render: {
1897
+ ...accessibleStored.render,
1898
+ props: historicalProps,
1899
+ epoch: pinnedEpoch,
1900
+ },
1901
+ };
1902
+ const served = await serveMount(uri, sessionId, pinnedRow);
1903
+ if (served !== null)
1904
+ return served;
1905
+ throw new ResourceReadFailure(failure);
1906
+ }
1907
+ }
1613
1908
  // Live state first: render present and renderable mounts with the
1614
1909
  // current props + current contextSpec values.
1615
1910
  if (accessibleStored) {
@@ -1797,8 +2092,9 @@ export function installMcpAppsOutbound(server, opts = {}) {
1797
2092
  // static registration's CSP-declaration fallback — deployments that
1798
2093
  // set no `publicBaseUrl` (it also feeds Origin/Host enforcement +
1799
2094
  // OAuth) still declare their origin to spec-compliant hosts.
1800
- registerGguiRenderResource(server, opts.shellHtml, opts.publicBaseUrl, opts.selfContained?.runtimeUrl);
2095
+ const shellResourceUri = registerGguiRenderResource(server, opts.shellHtml, opts.publicBaseUrl, opts.selfContained?.runtimeUrl, opts.extraConnectUrls);
1801
2096
  if (opts.selfContained) {
1802
2097
  registerGguiRenderResourceTemplate(server, opts.selfContained);
1803
2098
  }
2099
+ return { shellResourceUri };
1804
2100
  }
@@ -46,6 +46,22 @@ interface PerAppRouting {
46
46
  readonly pathPrefix?: string;
47
47
  readonly authorize?: (urlAppId: string, identity: AuthResult) => Promise<void>;
48
48
  }
49
+ /**
50
+ * Return shape for an `errorMapper` hook — a domain error mapped onto
51
+ * an HTTP/JSON-RPC response triple, with optional response headers.
52
+ *
53
+ * `headers` is deliberately generic HTTP plumbing (e.g. `Retry-After`
54
+ * on a `503`), not a deployment-specific concept — any operator
55
+ * mapping a domain error to a status code that conventionally carries
56
+ * a header can use it.
57
+ */
58
+ export interface ErrorMapperResult {
59
+ readonly status: number;
60
+ readonly code: number;
61
+ readonly message: string;
62
+ /** Response headers to set before the JSON body is written. */
63
+ readonly headers?: Readonly<Record<string, string>>;
64
+ }
49
65
  interface MountOptions {
50
66
  /** Express app to mount onto. */
51
67
  readonly app: Express;
@@ -79,11 +95,7 @@ interface MountOptions {
79
95
  /** Operator-configured issuer URL override (OAuth). */
80
96
  readonly oauthIssuerUrl?: string;
81
97
  /** Operator-supplied error → HTTP/JSON-RPC mapping hook. */
82
- readonly errorMapper?: (err: unknown) => {
83
- readonly status: number;
84
- readonly code: number;
85
- readonly message: string;
86
- } | undefined;
98
+ readonly errorMapper?: (err: unknown) => ErrorMapperResult | undefined;
87
99
  /**
88
100
  * Per-boot `buildMcpServer` options. Assembled once by the composer
89
101
  * (every input is fixed at composition time); the handler spreads a
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-endpoint-routes.d.ts","sourceRoot":"","sources":["../src/mcp-endpoint-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAElF,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAC1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AAEvC,OAAO,EAAkB,KAAK,qBAAqB,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAM7F,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGlD,sFAAsF;AACtF,UAAU,aAAa;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChF;AAED,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,iEAAiE;IACjE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;IAC1E;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;IACjF,sEAAsE;IACtE,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAChD,sEAAsE;IACtE,QAAQ,CAAC,GAAG,EAAE,iBAAiB,CAAC,cAAc,CAAC,CAAC;IAChD,qDAAqD;IACrD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,CAAC;IAC3D,wEAAwE;IACxE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC,iEAAiE;IACjE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,uDAAuD;IACvD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,CAAC,EAAE,CACrB,GAAG,EAAE,OAAO,KACT;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAC9F;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC;CACjD;AAyBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CA4U1D"}
1
+ {"version":3,"file":"mcp-endpoint-routes.d.ts","sourceRoot":"","sources":["../src/mcp-endpoint-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAElF,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAC1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AAEvC,OAAO,EAAkB,KAAK,qBAAqB,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAM7F,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGlD,sFAAsF;AACtF,UAAU,aAAa;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChF;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrD;AAED,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,iEAAiE;IACjE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;IAC1E;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;IACjF,sEAAsE;IACtE,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IAChD,sEAAsE;IACtE,QAAQ,CAAC,GAAG,EAAE,iBAAiB,CAAC,cAAc,CAAC,CAAC;IAChD,qDAAqD;IACrD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,CAAC;IAC3D,wEAAwE;IACxE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC,iEAAiE;IACjE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,uDAAuD;IACvD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,iBAAiB,GAAG,SAAS,CAAC;IACvE;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAC;CACjD;AAyBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CA2U1D"}
@@ -235,6 +235,8 @@ export function mountMcpEndpoints(opts) {
235
235
  }
236
236
  }
237
237
  if (mapped) {
238
+ if (mapped.headers)
239
+ res.set(mapped.headers);
238
240
  res.status(mapped.status).json({
239
241
  jsonrpc: "2.0",
240
242
  error: { code: mapped.code, message: mapped.message },