@guuey/mcp-apps-host 0.5.0 → 0.6.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.
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export { asGguiRender, asGguiRenderBootstrap, blockGguiRender, gguiRenderResourc
8
8
  export { resolveViewMount, snapshotViewMount, toolResultViewMount, type LocatorViewMount, type ResolvedViewMount, type UiResourceReader, type ViewMount, type ViewMountChannel, } from "./card-mount.js";
9
9
  export { createMcpUiResourceReader, uiResourceChannel, type CreateMcpUiResourceReaderDeps, type McpResourceReadResult, } from "./reader.js";
10
10
  export { asToolCallResult, createMcpUiActionRelay, unavailableToolCallResult, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, type CreateMcpUiActionRelayDeps, type McpToolCallContent, type McpToolCallResult, type McpToolStructuredContent, type UiActionRequest, } from "./action.js";
11
- export { initializeResult, initialViewHostState, teardownMessage, toolCallResponse, TOOLS_CALL_METHOD, viewHostElapsed, viewHostReceive, type ViewHostBehavior, type ViewHostEffect, type ViewHostOutbound, type ViewHostPhase, type ViewHostInfo, type ViewHostState, type ViewHostTransition, type ViewRequestId, } from "./view-host-protocol.js";
11
+ export { initializeResult, initialViewHostState, resourceReadResponse, RESOURCES_READ_METHOD, teardownMessage, toolCallResponse, TOOLS_CALL_METHOD, viewHostElapsed, viewHostReceive, type ViewHostBehavior, type ViewHostEffect, type ViewHostOutbound, type ViewHostPhase, type ViewHostInfo, type ViewHostState, type ViewHostTransition, type ViewRequestId, } from "./view-host-protocol.js";
12
12
  export { attachViewHost, viewDocumentHtml, type AttachViewHostConfig, type ViewFrameLike, type ViewHostEvents, } from "./view-host.js";
13
13
  export { attachSandboxPageDelivery, isSandboxProxyReady, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, type SandboxPageDeliveryConfig, } from "./sandbox-page.js";
14
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,4BAA4B,EAC5B,kBAAkB,EAClB,oBAAoB,EACpB,SAAS,EACT,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EACf,0BAA0B,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,0BAA0B,EAC1B,6BAA6B,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,4BAA4B,EAC5B,kBAAkB,EAClB,oBAAoB,EACpB,SAAS,EACT,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EACf,0BAA0B,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,0BAA0B,EAC1B,6BAA6B,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -8,6 +8,6 @@ export { asGguiRender, asGguiRenderBootstrap, blockGguiRender, gguiRenderResourc
8
8
  export { resolveViewMount, snapshotViewMount, toolResultViewMount, } from "./card-mount.js";
9
9
  export { createMcpUiResourceReader, uiResourceChannel, } from "./reader.js";
10
10
  export { asToolCallResult, createMcpUiActionRelay, unavailableToolCallResult, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, } from "./action.js";
11
- export { initializeResult, initialViewHostState, teardownMessage, toolCallResponse, TOOLS_CALL_METHOD, viewHostElapsed, viewHostReceive, } from "./view-host-protocol.js";
11
+ export { initializeResult, initialViewHostState, resourceReadResponse, RESOURCES_READ_METHOD, teardownMessage, toolCallResponse, TOOLS_CALL_METHOD, viewHostElapsed, viewHostReceive, } from "./view-host-protocol.js";
12
12
  export { attachViewHost, viewDocumentHtml, } from "./view-host.js";
13
13
  export { attachSandboxPageDelivery, isSandboxProxyReady, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, } from "./sandbox-page.js";
package/dist/react.d.ts CHANGED
@@ -41,7 +41,7 @@ export type { AttachViewHostConfig, ViewFrameLike, ViewHostEvents } from "./view
41
41
  export { attachSandboxPageDelivery, isSandboxProxyReady, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, type SandboxPageDeliveryConfig, } from "./sandbox-page.js";
42
42
  export type { ViewHostPhase } from "./view-host-protocol.js";
43
43
  export type { ResolvedViewMount, ViewMount, ViewMountChannel } from "./card-mount.js";
44
- export interface GuueyViewProps extends Pick<AttachViewHostConfig, "hostCapabilities" | "hostInfo" | "hostContext" | "onCallTool" | "negotiationTimeoutMs"> {
44
+ export interface GuueyViewProps extends Pick<AttachViewHostConfig, "hostCapabilities" | "hostInfo" | "hostContext" | "onCallTool" | "onReadResource" | "onSizeChanged" | "negotiationTimeoutMs"> {
45
45
  /** The resolved card to mount (see `toolResultViewMount`/`resolveViewMount`). */
46
46
  mount: ResolvedViewMount;
47
47
  /**
@@ -59,8 +59,23 @@ export interface GuueyViewProps extends Pick<AttachViewHostConfig, "hostCapabili
59
59
  * for its CSP + referrer checks to mean anything), never agent HTML.
60
60
  * The page must be a genuinely different origin; a same-origin URL is
61
61
  * refused with a labeled state, never mounted.
62
+ *
63
+ * `null` (as opposed to absent) means the two-origin mount is REQUIRED
64
+ * by the embedder's posture but no page is configured — the mount is
65
+ * refused with the same labeled state, and srcdoc is NEVER fallen back
66
+ * to (falling back would silently trade the caller's egress confinement
67
+ * for the page's CSP; the widget/Studio convergence posture).
62
68
  */
63
- sandboxPageUrl?: string;
69
+ sandboxPageUrl?: string | null;
70
+ /**
71
+ * Apply the view's own size reports (`ui/notifications/size-changed` —
72
+ * spec surface) to the frame: a reported HEIGHT becomes the frame's
73
+ * height; width stays the container's (a transcript column owns its
74
+ * width). Default OFF — the primitive changes nothing for existing
75
+ * hosts; a caller's {@link AttachViewHostConfig.onSizeChanged} observer
76
+ * fires either way.
77
+ */
78
+ autoResize?: boolean;
64
79
  /**
65
80
  * Sandbox flags appended to the safe default (`allow-scripts`). Every
66
81
  * entry widens what agent-generated HTML may do — `allow-same-origin`
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,EAAwC,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjG,OAAO,EAAoC,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE7F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClE,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC1F,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,0BAA0B,EAC1B,6BAA6B,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,iBAAiB,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAKtF,MAAM,WAAW,cACf,SAAQ,IAAI,CACV,oBAAoB,EACpB,kBAAkB,GAAG,UAAU,GAAG,aAAa,GAAG,YAAY,GAAG,sBAAsB,CACxF;IACD,iFAAiF;IACjF,KAAK,EAAE,iBAAiB,CAAC;IACzB;;;;;;;;;;;;;;;OAeG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,0BAA0B,CAAC,EAAE,MAAM,EAAE,CAAC;IACtC,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,SAAS,CAAC;CACpD;AA+BD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,CAiI1D"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,EAAwC,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjG,OAAO,EAAoC,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE7F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClE,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC1F,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,0BAA0B,EAC1B,6BAA6B,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,iBAAiB,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAKtF,MAAM,WAAW,cACf,SAAQ,IAAI,CACV,oBAAoB,EAClB,kBAAkB,GAClB,UAAU,GACV,aAAa,GACb,YAAY,GACZ,gBAAgB,GAChB,eAAe,GACf,sBAAsB,CACzB;IACD,iFAAiF;IACjF,KAAK,EAAE,iBAAiB,CAAC;IACzB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;OAMG;IACH,0BAA0B,CAAC,EAAE,MAAM,EAAE,CAAC;IACtC,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,SAAS,CAAC;CACpD;AA+BD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,CA0J1D"}
package/dist/react.js CHANGED
@@ -68,18 +68,23 @@ function defaultStatus(phase, channel) {
68
68
  * docblock for the sandbox and state contracts.
69
69
  */
70
70
  export function GuueyView(props) {
71
- const { mount, sandboxPageUrl, dangerouslyAddSandboxFlags, allow, title, className, style, onPhaseChange, renderStatus, ...hostConfig } = props;
71
+ const { mount, sandboxPageUrl, autoResize, dangerouslyAddSandboxFlags, allow, title, className, style, onPhaseChange, renderStatus, ...hostConfig } = props;
72
72
  const frameRef = useRef(null);
73
73
  const [phase, setPhase] = useState("negotiating");
74
+ // The view's own size report, applied only under `autoResize`.
75
+ const [reportedHeight, setReportedHeight] = useState(undefined);
74
76
  const html = viewDocumentHtml(mount.resource);
75
77
  // Vet the sandbox page once per URL. Same-origin is REFUSED (the widget's
76
78
  // ResourceMount precedent, generalized): the whole point of the page is
77
79
  // being a different origin — same-origin would hand the relay page (and
78
80
  // through `allow-same-origin`, everything it can reach) the embedder's
79
- // own origin.
81
+ // own origin. `null` — page mode required but unconfigured — refuses the
82
+ // same way: srcdoc is never a silent fallback for a confinement posture.
80
83
  const sandboxPage = useMemo(() => {
81
84
  if (sandboxPageUrl === undefined)
82
85
  return undefined;
86
+ if (sandboxPageUrl === null)
87
+ return "refused";
83
88
  let url;
84
89
  try {
85
90
  url = new URL(sandboxPageUrl);
@@ -95,13 +100,14 @@ export function GuueyView(props) {
95
100
  // The attachment is keyed to the mounted DOCUMENT, not to every render's
96
101
  // fresh callback identities — host config rides a ref so the effect's
97
102
  // dependency list is honestly just the document identity.
98
- const latest = useRef({ hostConfig, onPhaseChange, dangerouslyAddSandboxFlags });
99
- latest.current = { hostConfig, onPhaseChange, dangerouslyAddSandboxFlags };
103
+ const latest = useRef({ hostConfig, onPhaseChange, dangerouslyAddSandboxFlags, autoResize });
104
+ latest.current = { hostConfig, onPhaseChange, dangerouslyAddSandboxFlags, autoResize };
100
105
  useEffect(() => {
101
106
  // Keyed to the same identity the frame is (the resource uri): a new
102
107
  // document boots fresh, and the previous negotiation's phase must not
103
- // paper over it.
108
+ // paper over it — nor must the previous document's reported size.
104
109
  setPhase("negotiating");
110
+ setReportedHeight(undefined);
105
111
  const frame = frameRef.current;
106
112
  if (frame === null || html === undefined)
107
113
  return;
@@ -115,6 +121,12 @@ export function GuueyView(props) {
115
121
  setPhase(next);
116
122
  latest.current.onPhaseChange?.(next);
117
123
  },
124
+ onSizeChanged: (size) => {
125
+ if (latest.current.autoResize === true && size.height !== undefined) {
126
+ setReportedHeight(size.height);
127
+ }
128
+ latest.current.hostConfig.onSizeChanged?.(size);
129
+ },
118
130
  });
119
131
  if (page === undefined)
120
132
  return detachHost;
@@ -141,9 +153,14 @@ export function GuueyView(props) {
141
153
  return (_jsx("div", { className: className, style: { position: "relative", ...style }, children: _jsx("p", { role: "alert", style: { ...statusLineStyle, opacity: 1, pointerEvents: "auto" }, children: "This view could not be displayed \u2014 its resource carries no document." }) }));
142
154
  }
143
155
  if (sandboxPageUrl !== undefined && page === undefined) {
144
- // A malformed or SAME-ORIGIN sandbox page is a configuration state, not
145
- // a property of the card — refused, labeled, never mounted.
146
- return (_jsx("div", { className: className, style: { position: "relative", ...style }, children: _jsx("p", { role: "alert", style: { ...statusLineStyle, opacity: 1, pointerEvents: "auto" }, children: "Interactive view unavailable \u2014 the sandbox page is not usable from this origin." }) }));
156
+ // A missing (null), malformed, or SAME-ORIGIN sandbox page is a
157
+ // configuration state, not a property of the card — refused, labeled,
158
+ // never mounted, and never silently downgraded to srcdoc. The copy
159
+ // names the configuration cause (an operator can act on it) without
160
+ // ever printing the offending URL.
161
+ return (_jsx("div", { className: className, style: { position: "relative", ...style }, children: _jsx("p", { role: "alert", style: { ...statusLineStyle, opacity: 1, pointerEvents: "auto" }, children: sandboxPageUrl === null
162
+ ? "Interactive view unavailable — no sandbox page is configured."
163
+ : "Interactive view unavailable — the sandbox page is not usable from this origin." }) }));
147
164
  }
148
165
  return (_jsxs("div", { className: className, style: { position: "relative", ...style }, children: [_jsx("iframe", { ref: frameRef, ...(page !== undefined ? { src: page.href } : { srcDoc: html }), title: title ?? DEFAULT_TITLE,
149
166
  // srcdoc mode: the INVARIANT — agent HTML in an opaque origin, extra
@@ -154,5 +171,14 @@ export function GuueyView(props) {
154
171
  // and the caller's extra flags travel to THAT frame via the relay.
155
172
  sandbox: page !== undefined
156
173
  ? "allow-scripts allow-same-origin allow-forms"
157
- : ["allow-scripts", ...(dangerouslyAddSandboxFlags ?? [])].join(" "), allow: allow ?? "clipboard-write", style: { display: "block", width: "100%", height: "100%", border: 0 } }, `${page?.href ?? "srcdoc"}::${mount.resource.uri}`), renderStatus !== undefined ? renderStatus(phase) : defaultStatus(phase, mount.channel)] }));
174
+ : ["allow-scripts", ...(dangerouslyAddSandboxFlags ?? [])].join(" "), allow: allow ?? "clipboard-write",
175
+ // Under `autoResize`, the view's own height report wins over the
176
+ // fill-the-container default (width stays the container's — a
177
+ // transcript column owns its width).
178
+ style: {
179
+ display: "block",
180
+ width: "100%",
181
+ height: autoResize === true && reportedHeight !== undefined ? reportedHeight : "100%",
182
+ border: 0,
183
+ } }, `${page?.href ?? "srcdoc"}::${mount.resource.uri}`), renderStatus !== undefined ? renderStatus(phase) : defaultStatus(phase, mount.channel)] }));
158
184
  }
@@ -32,6 +32,7 @@
32
32
  */
33
33
  import { type McpUiHostCapabilities, type McpUiHostContext, type McpUiInitializeResult } from "@modelcontextprotocol/ext-apps";
34
34
  import type { McpToolStructuredContent } from "./action.js";
35
+ import type { McpResourceReadResult } from "./reader.js";
35
36
  /**
36
37
  * The host identity in the initialize result — structurally the spec's
37
38
  * `Implementation` (which the ext-apps root does not re-export), narrowed
@@ -43,6 +44,15 @@ export interface ViewHostInfo {
43
44
  }
44
45
  /** The standard MCP method a view uses to reach host-proxied tools. */
45
46
  export declare const TOOLS_CALL_METHOD = "tools/call";
47
+ /**
48
+ * The standard MCP method a view uses to read host-proxied resources —
49
+ * `ReadResourceRequest` in the spec's App→Host request union
50
+ * (`@modelcontextprotocol/ext-apps` `AppRequest`). A local constant, same
51
+ * as {@link TOOLS_CALL_METHOD}: the string is MCP-core vocabulary the
52
+ * ext-apps root does not re-export, and this package deliberately carries
53
+ * no `@modelcontextprotocol/sdk` dependency.
54
+ */
55
+ export declare const RESOURCES_READ_METHOD = "resources/read";
46
56
  /** A JSON-RPC id as the wire allows it. */
47
57
  export type ViewRequestId = number | string;
48
58
  /** The messages this host posts INTO the view frame. */
@@ -105,6 +115,31 @@ export type ViewHostEffect = {
105
115
  id: ViewRequestId;
106
116
  name: string;
107
117
  arguments?: McpToolStructuredContent;
118
+ } | {
119
+ /**
120
+ * A `resources/read` the config accepted for relaying (spec surface:
121
+ * `ReadResourceRequest` rides the App→Host union, and the matching
122
+ * advertisement is `hostCapabilities.serverResources`). The glue runs
123
+ * the read hook and posts {@link resourceReadResponse}. Only emitted
124
+ * when {@link ViewHostBehavior.resourceRelay} is true — unwired, the
125
+ * machine refuses in-band like every other unsupported request.
126
+ */
127
+ kind: "relay-resource-read";
128
+ id: ViewRequestId;
129
+ uri: string;
130
+ } | {
131
+ /**
132
+ * The view reported its content size (`ui/notifications/size-changed`
133
+ * — spec notification, App → Host). At least one of the two fields is
134
+ * a finite number; a notification carrying neither is consumed
135
+ * silently instead. The glue forwards this to the embedder
136
+ * ({@link AttachViewHostConfig.onSizeChanged} in `view-host.ts`) —
137
+ * whether/how to resize the frame is the embedder's layout decision,
138
+ * never the machine's.
139
+ */
140
+ kind: "size-changed";
141
+ width?: number;
142
+ height?: number;
108
143
  };
109
144
  /**
110
145
  * The host identity/behavior the machine answers with. Everything here is
@@ -125,6 +160,8 @@ export interface ViewHostBehavior {
125
160
  hostContext: McpUiHostContext;
126
161
  /** Whether a `tools/call` relay hook is wired (see `view-host.ts`). */
127
162
  toolRelay: boolean;
163
+ /** Whether a `resources/read` relay hook is wired (see `view-host.ts`). */
164
+ resourceRelay: boolean;
128
165
  }
129
166
  /** The result of feeding one inbound frame (or the timeout) to the machine. */
130
167
  export interface ViewHostTransition {
@@ -137,6 +174,13 @@ export declare function initializeResult(behavior: ViewHostBehavior, requestedPr
137
174
  export declare function toolCallResponse(id: ViewRequestId, result: {
138
175
  [key: string]: unknown;
139
176
  }): ViewHostOutbound;
177
+ /**
178
+ * Build the in-band response for a relayed `resources/read`. An entry
179
+ * becomes the spec's `ReadResourceResult` (`contents: [entry]`); `undefined`
180
+ * — a miss, a deny, or a relay failure alike — becomes the one
181
+ * `Resource not found` error (deny == miss, {@link RESOURCE_NOT_FOUND}).
182
+ */
183
+ export declare function resourceReadResponse(id: ViewRequestId, entry: McpResourceReadResult | undefined): ViewHostOutbound;
140
184
  /**
141
185
  * The spec-mannered farewell a detaching host posts (`ui/resource-teardown`).
142
186
  * Sent WITHOUT an id — a host that is tearing the frame down cannot await a
@@ -1 +1 @@
1
- {"version":3,"file":"view-host-protocol.d.ts","sourceRoot":"","sources":["../src/view-host-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAIL,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC3B,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAE5D;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAKD,uEAAuE;AACvE,eAAO,MAAM,iBAAiB,eAAe,CAAC;AAE9C,2CAA2C;AAC3C,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,CAAC,EAAE,aAAa,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IACpC,MAAM,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IACpC,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,WAAW,GAAG,cAAc,CAAC;AAEzE,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,aAAa,CAAC;IACrB,0EAA0E;IAC1E,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,oBAAoB,IAAI,aAAa,CAEpD;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,gBAAgB,CAAA;CAAE,GAC9C;IACE;;;;;;OAMG;IACH,IAAI,EAAE,iBAAiB,CAAC;IACxB,EAAE,EAAE,aAAa,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,wBAAwB,CAAC;CACtC,CAAC;AAEN;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,YAAY,CAAC;IACvB;;;;;;;OAOG;IACH,gBAAgB,EAAE,qBAAqB,CAAC;IACxC,+DAA+D;IAC/D,WAAW,EAAE,gBAAgB,CAAC;IAC9B,uEAAuE;IACvE,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,+EAA+E;AAC/E,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,aAAa,CAAC;IACrB,OAAO,EAAE,cAAc,EAAE,CAAC;CAC3B;AAiCD,iFAAiF;AACjF,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,gBAAgB,EAC1B,wBAAwB,EAAE,OAAO,GAChC,qBAAqB,CAcvB;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,aAAa,EACjB,MAAM,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GACjC,gBAAgB,CAElB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,gBAAgB,CAElD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,aAAa,EACpB,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,OAAO,GACZ,kBAAkB,CAuDpB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,aAAa,CAEnE"}
1
+ {"version":3,"file":"view-host-protocol.d.ts","sourceRoot":"","sources":["../src/view-host-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAKL,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC3B,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEzD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAKD,uEAAuE;AACvE,eAAO,MAAM,iBAAiB,eAAe,CAAC;AAE9C;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,mBAAmB,CAAC;AAStD,2CAA2C;AAC3C,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,CAAC,EAAE,aAAa,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IACpC,MAAM,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IACpC,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,WAAW,GAAG,cAAc,CAAC;AAEzE,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,aAAa,CAAC;IACrB,0EAA0E;IAC1E,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,oBAAoB,IAAI,aAAa,CAEpD;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,gBAAgB,CAAA;CAAE,GAC9C;IACE;;;;;;OAMG;IACH,IAAI,EAAE,iBAAiB,CAAC;IACxB,EAAE,EAAE,aAAa,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,wBAAwB,CAAC;CACtC,GACD;IACE;;;;;;;OAOG;IACH,IAAI,EAAE,qBAAqB,CAAC;IAC5B,EAAE,EAAE,aAAa,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,GACD;IACE;;;;;;;;OAQG;IACH,IAAI,EAAE,cAAc,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEN;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,YAAY,CAAC;IACvB;;;;;;;OAOG;IACH,gBAAgB,EAAE,qBAAqB,CAAC;IACxC,+DAA+D;IAC/D,WAAW,EAAE,gBAAgB,CAAC;IAC9B,uEAAuE;IACvE,SAAS,EAAE,OAAO,CAAC;IACnB,2EAA2E;IAC3E,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,+EAA+E;AAC/E,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,aAAa,CAAC;IACrB,OAAO,EAAE,cAAc,EAAE,CAAC;CAC3B;AAiCD,iFAAiF;AACjF,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,gBAAgB,EAC1B,wBAAwB,EAAE,OAAO,GAChC,qBAAqB,CAcvB;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,aAAa,EACjB,MAAM,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GACjC,gBAAgB,CAElB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE,qBAAqB,GAAG,SAAS,GACvC,gBAAgB,CASlB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,gBAAgB,CAElD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,aAAa,EACpB,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,OAAO,GACZ,kBAAkB,CAuFpB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,aAAa,CAEnE"}
@@ -30,11 +30,26 @@
30
30
  * view; everything ggui-specific stays in `ggui-render.ts` behind ggui's
31
31
  * own published protocol package.
32
32
  */
33
- import { INITIALIZE_METHOD, LATEST_PROTOCOL_VERSION, RESOURCE_TEARDOWN_METHOD, } from "@modelcontextprotocol/ext-apps";
33
+ import { INITIALIZE_METHOD, LATEST_PROTOCOL_VERSION, RESOURCE_TEARDOWN_METHOD, SIZE_CHANGED_METHOD, } from "@modelcontextprotocol/ext-apps";
34
34
  /** JSON-RPC `method not found` — the spec's code, not an invented one. */
35
35
  const METHOD_NOT_SUPPORTED = -32601;
36
36
  /** The standard MCP method a view uses to reach host-proxied tools. */
37
37
  export const TOOLS_CALL_METHOD = "tools/call";
38
+ /**
39
+ * The standard MCP method a view uses to read host-proxied resources —
40
+ * `ReadResourceRequest` in the spec's App→Host request union
41
+ * (`@modelcontextprotocol/ext-apps` `AppRequest`). A local constant, same
42
+ * as {@link TOOLS_CALL_METHOD}: the string is MCP-core vocabulary the
43
+ * ext-apps root does not re-export, and this package deliberately carries
44
+ * no `@modelcontextprotocol/sdk` dependency.
45
+ */
46
+ export const RESOURCES_READ_METHOD = "resources/read";
47
+ /**
48
+ * MCP's `Resource not found` JSON-RPC code — the one answer for a miss, a
49
+ * transport deny, AND a relay failure (deny == miss: the reader discipline,
50
+ * `reader.ts` — the view gets no oracle for which locators resolve).
51
+ */
52
+ const RESOURCE_NOT_FOUND = -32002;
38
53
  export function initialViewHostState() {
39
54
  return { phase: "negotiating", initializedSeen: false };
40
55
  }
@@ -83,6 +98,22 @@ export function initializeResult(behavior, requestedProtocolVersion) {
83
98
  export function toolCallResponse(id, result) {
84
99
  return { jsonrpc: "2.0", id, result };
85
100
  }
101
+ /**
102
+ * Build the in-band response for a relayed `resources/read`. An entry
103
+ * becomes the spec's `ReadResourceResult` (`contents: [entry]`); `undefined`
104
+ * — a miss, a deny, or a relay failure alike — becomes the one
105
+ * `Resource not found` error (deny == miss, {@link RESOURCE_NOT_FOUND}).
106
+ */
107
+ export function resourceReadResponse(id, entry) {
108
+ if (entry === undefined) {
109
+ return {
110
+ jsonrpc: "2.0",
111
+ id,
112
+ error: { code: RESOURCE_NOT_FOUND, message: "resource unavailable" },
113
+ };
114
+ }
115
+ return { jsonrpc: "2.0", id, result: { contents: [entry] } };
116
+ }
86
117
  /**
87
118
  * The spec-mannered farewell a detaching host posts (`ui/resource-teardown`).
88
119
  * Sent WITHOUT an id — a host that is tearing the frame down cannot await a
@@ -110,10 +141,29 @@ export function viewHostReceive(state, behavior, data) {
110
141
  if (req === undefined)
111
142
  return { state, effects: [] };
112
143
  if (req.id === undefined) {
113
- // A notification. Track the one the handshake defines; consume the rest.
144
+ // A notification. Track the one the handshake defines, surface the one
145
+ // the embedder may act on; consume the rest.
114
146
  if (req.method === "ui/notifications/initialized" && !state.initializedSeen) {
115
147
  return { state: { ...state, initializedSeen: true }, effects: [] };
116
148
  }
149
+ if (req.method === SIZE_CHANGED_METHOD) {
150
+ const width = req.params?.["width"];
151
+ const height = req.params?.["height"];
152
+ const validWidth = typeof width === "number" && Number.isFinite(width);
153
+ const validHeight = typeof height === "number" && Number.isFinite(height);
154
+ if (validWidth || validHeight) {
155
+ return {
156
+ state,
157
+ effects: [
158
+ {
159
+ kind: "size-changed",
160
+ ...(validWidth ? { width } : {}),
161
+ ...(validHeight ? { height } : {}),
162
+ },
163
+ ],
164
+ };
165
+ }
166
+ }
117
167
  return { state, effects: [] };
118
168
  }
119
169
  if (req.method === INITIALIZE_METHOD) {
@@ -141,6 +191,18 @@ export function viewHostReceive(state, behavior, data) {
141
191
  }
142
192
  // fall through: a nameless tools/call is not a call we can relay.
143
193
  }
194
+ if (req.method === RESOURCES_READ_METHOD && behavior.resourceRelay) {
195
+ const uri = req.params?.["uri"];
196
+ if (typeof uri === "string") {
197
+ return { state, effects: [{ kind: "relay-resource-read", id: req.id, uri }] };
198
+ }
199
+ // fall through: a uri-less read is not a read we can relay.
200
+ }
201
+ const answered = [
202
+ INITIALIZE_METHOD,
203
+ ...(behavior.toolRelay ? [TOOLS_CALL_METHOD] : []),
204
+ ...(behavior.resourceRelay ? [RESOURCES_READ_METHOD] : []),
205
+ ];
144
206
  return {
145
207
  state,
146
208
  effects: [
@@ -151,7 +213,7 @@ export function viewHostReceive(state, behavior, data) {
151
213
  id: req.id,
152
214
  error: {
153
215
  code: METHOD_NOT_SUPPORTED,
154
- message: `method_not_supported: ${req.method} — this host answers ${INITIALIZE_METHOD}${behavior.toolRelay ? ` and ${TOOLS_CALL_METHOD}` : ""} only`,
216
+ message: `method_not_supported: ${req.method} — this host answers ${answered.join(", ")} only`,
155
217
  },
156
218
  },
157
219
  },
@@ -28,6 +28,7 @@
28
28
  * re-derived here against the pure machine + our own tests.
29
29
  */
30
30
  import { type ViewHostPhase } from "./view-host-protocol.js";
31
+ import type { McpResourceReadResult } from "./reader.js";
31
32
  import { type McpToolCallResult, type UiActionRequest } from "./action.js";
32
33
  import type { McpUiResourcePayload } from "./block-ui.js";
33
34
  import type { McpUiHostCapabilities, McpUiHostContext } from "@modelcontextprotocol/ext-apps";
@@ -91,6 +92,28 @@ export interface AttachViewHostConfig {
91
92
  * `<GuueyView>` fills it from the mount automatically.
92
93
  */
93
94
  resourceUri?: string;
95
+ /**
96
+ * The `resources/read` relay — a PRIVILEGE boundary like
97
+ * {@link onCallTool}, default off: with no hook, the machine refuses
98
+ * `resources/read` in-band and advertises no `serverResources`. The hook
99
+ * is structurally the SAME transport `createMcpUiResourceReader`
100
+ * assembles over ({@link CreateMcpUiResourceReaderDeps.readResource}) —
101
+ * a host with a locator reader wires the identical function here. Trust
102
+ * rules ride the reader discipline (`reader.ts`): enforcement lives
103
+ * INSIDE the transport; a miss, a deny, and a throw all answer the view
104
+ * with the one `Resource not found` error (deny == miss — no oracle).
105
+ */
106
+ onReadResource?: (uri: string) => Promise<McpResourceReadResult | undefined>;
107
+ /**
108
+ * The view reported its content size (`ui/notifications/size-changed` —
109
+ * spec notification). Whether and how to resize the frame is the
110
+ * embedder's layout decision; `<GuueyView autoResize>` is one wiring of
111
+ * exactly this callback.
112
+ */
113
+ onSizeChanged?: (size: {
114
+ width?: number;
115
+ height?: number;
116
+ }) => void;
94
117
  /** Observe phase transitions (see {@link ViewHostPhase}). */
95
118
  onPhaseChange?: (phase: ViewHostPhase) => void;
96
119
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"view-host.d.ts","sourceRoot":"","sources":["../src/view-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAQL,KAAK,aAAa,EAEnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAEL,KAAK,iBAAiB,EAEtB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAC9F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,aAAa,EAAE;QAAE,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IAC7F,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,4DAA4D;AAC5D,MAAM,WAAW,cAAc;IAC7B,gBAAgB,CACd,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAC5D,IAAI,CAAC;IACR,mBAAmB,CACjB,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAC5D,IAAI,CAAC;CACT;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;IACzC,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACtE;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qEAAqE;IACrE,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AA6BD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,GAAE,oBAAyB,GAAG,MAAM,IAAI,CAgElG;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,MAAM,GAAG,SAAS,CAcnF"}
1
+ {"version":3,"file":"view-host.d.ts","sourceRoot":"","sources":["../src/view-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EASL,KAAK,aAAa,EAEnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAEL,KAAK,iBAAiB,EAEtB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAC9F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,aAAa,EAAE;QAAE,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IAC7F,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,4DAA4D;AAC5D,MAAM,WAAW,cAAc;IAC7B,gBAAgB,CACd,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAC5D,IAAI,CAAC;IACR,mBAAmB,CACjB,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAC5D,IAAI,CAAC;CACT;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;IACzC,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACtE;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;;OAUG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAAC;IAC7E;;;;;OAKG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACpE,6DAA6D;IAC7D,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qEAAqE;IACrE,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAoDD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,GAAE,oBAAyB,GAAG,MAAM,IAAI,CAkFlG;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,MAAM,GAAG,SAAS,CAcnF"}
package/dist/view-host.js CHANGED
@@ -27,7 +27,7 @@
27
27
  * Seeded from ggui's console `surface-host.ts` (donated, guuey#186 audit);
28
28
  * re-derived here against the pure machine + our own tests.
29
29
  */
30
- import { initialViewHostState, teardownMessage, toolCallResponse, viewHostElapsed, viewHostReceive, } from "./view-host-protocol.js";
30
+ import { initialViewHostState, resourceReadResponse, teardownMessage, toolCallResponse, viewHostElapsed, viewHostReceive, } from "./view-host-protocol.js";
31
31
  import { unavailableToolCallResult, } from "./action.js";
32
32
  const DEFAULT_HOST_INFO = { name: "guuey-view-host", version: "1" };
33
33
  const DEFAULT_NEGOTIATION_TIMEOUT_MS = 8000;
@@ -43,14 +43,38 @@ function hostContextFor(frame, config) {
43
43
  }
44
44
  function behaviorFor(frame, config) {
45
45
  const relayWired = config.onCallTool !== undefined && config.resourceUri !== undefined;
46
+ const readWired = config.onReadResource !== undefined;
46
47
  return {
47
48
  hostInfo: config.hostInfo ?? DEFAULT_HOST_INFO,
48
49
  hostCapabilities: {
50
+ // A wired relay IS the implementation — advertise it; an explicit
51
+ // hostCapabilities entry still wins (the serverTools precedent).
49
52
  ...(relayWired ? { serverTools: {} } : {}),
53
+ ...(readWired ? { serverResources: {} } : {}),
50
54
  ...config.hostCapabilities,
51
55
  },
52
56
  hostContext: hostContextFor(frame, config),
53
57
  toolRelay: relayWired,
58
+ resourceRelay: readWired,
59
+ };
60
+ }
61
+ /**
62
+ * Re-narrow a read hook's answer at the trust boundary — hooks are embedder
63
+ * code (possibly plain JS), and the wire entry the view receives must be a
64
+ * real `contents[]` entry: `uri` required, a string payload arm required
65
+ * (a payload-less entry is a miss — the `createMcpUiResourceReader`
66
+ * discipline, applied to the WIRE entry rather than the mountable payload).
67
+ */
68
+ function narrowReadEntry(entry) {
69
+ if (entry === undefined || typeof entry.uri !== "string")
70
+ return undefined;
71
+ if (typeof entry.text !== "string" && typeof entry.blob !== "string")
72
+ return undefined;
73
+ return {
74
+ uri: entry.uri,
75
+ ...(typeof entry.mimeType === "string" ? { mimeType: entry.mimeType } : {}),
76
+ ...(typeof entry.text === "string" ? { text: entry.text } : {}),
77
+ ...(typeof entry.blob === "string" ? { blob: entry.blob } : {}),
54
78
  };
55
79
  }
56
80
  /**
@@ -84,6 +108,15 @@ export function attachViewHost(frame, config = {}) {
84
108
  // same in-band unavailable the relay itself uses, never a hang.
85
109
  () => post(toolCallResponse(id, unavailableToolCallResult())));
86
110
  };
111
+ const relayRead = (id, uri) => {
112
+ const { onReadResource } = config;
113
+ if (onReadResource === undefined)
114
+ return; // machine-guarded invariant, as with `relay`
115
+ onReadResource(uri).then((entry) => post(resourceReadResponse(id, narrowReadEntry(entry))),
116
+ // A throwing hook still owes the view an answer — the same not-found
117
+ // the reader discipline gives a deny (deny == miss), never a hang.
118
+ () => post(resourceReadResponse(id, undefined)));
119
+ };
87
120
  const onMessage = (event) => {
88
121
  if (frame.contentWindow === null || event.source !== frame.contentWindow)
89
122
  return;
@@ -92,8 +125,16 @@ export function attachViewHost(frame, config = {}) {
92
125
  for (const effect of effects) {
93
126
  if (effect.kind === "respond")
94
127
  post(effect.message);
95
- else
128
+ else if (effect.kind === "relay-tool-call")
96
129
  relay(effect.id, effect.name, effect.arguments);
130
+ else if (effect.kind === "relay-resource-read")
131
+ relayRead(effect.id, effect.uri);
132
+ else {
133
+ config.onSizeChanged?.({
134
+ ...(effect.width !== undefined ? { width: effect.width } : {}),
135
+ ...(effect.height !== undefined ? { height: effect.height } : {}),
136
+ });
137
+ }
97
138
  }
98
139
  };
99
140
  // One listener, two subscription paths: the injectable seam for Node
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guuey/mcp-apps-host",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "The MCP Apps (SEP-1865) Host role for guuey's chat surfaces — view-mount narrowing across UI channels, ui:// locator rehydration by resources/read, and the sandbox-trust channel contract. Vendor-neutral: any spec-following MCP App mounts through it.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  "dependencies": {
36
36
  "@ggui-ai/protocol": "0.9.0",
37
37
  "@modelcontextprotocol/ext-apps": "1.7.5",
38
- "@silverprotocol/core": "0.4.1"
38
+ "@silverprotocol/core": "0.5.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "react": ">=18"
package/src/index.ts CHANGED
@@ -58,6 +58,8 @@ export {
58
58
  export {
59
59
  initializeResult,
60
60
  initialViewHostState,
61
+ resourceReadResponse,
62
+ RESOURCES_READ_METHOD,
61
63
  teardownMessage,
62
64
  toolCallResponse,
63
65
  TOOLS_CALL_METHOD,
package/src/react.tsx CHANGED
@@ -56,7 +56,13 @@ const DEFAULT_TITLE = "Generated view";
56
56
  export interface GuueyViewProps
57
57
  extends Pick<
58
58
  AttachViewHostConfig,
59
- "hostCapabilities" | "hostInfo" | "hostContext" | "onCallTool" | "negotiationTimeoutMs"
59
+ | "hostCapabilities"
60
+ | "hostInfo"
61
+ | "hostContext"
62
+ | "onCallTool"
63
+ | "onReadResource"
64
+ | "onSizeChanged"
65
+ | "negotiationTimeoutMs"
60
66
  > {
61
67
  /** The resolved card to mount (see `toolResultViewMount`/`resolveViewMount`). */
62
68
  mount: ResolvedViewMount;
@@ -75,8 +81,23 @@ export interface GuueyViewProps
75
81
  * for its CSP + referrer checks to mean anything), never agent HTML.
76
82
  * The page must be a genuinely different origin; a same-origin URL is
77
83
  * refused with a labeled state, never mounted.
84
+ *
85
+ * `null` (as opposed to absent) means the two-origin mount is REQUIRED
86
+ * by the embedder's posture but no page is configured — the mount is
87
+ * refused with the same labeled state, and srcdoc is NEVER fallen back
88
+ * to (falling back would silently trade the caller's egress confinement
89
+ * for the page's CSP; the widget/Studio convergence posture).
78
90
  */
79
- sandboxPageUrl?: string;
91
+ sandboxPageUrl?: string | null;
92
+ /**
93
+ * Apply the view's own size reports (`ui/notifications/size-changed` —
94
+ * spec surface) to the frame: a reported HEIGHT becomes the frame's
95
+ * height; width stays the container's (a transcript column owns its
96
+ * width). Default OFF — the primitive changes nothing for existing
97
+ * hosts; a caller's {@link AttachViewHostConfig.onSizeChanged} observer
98
+ * fires either way.
99
+ */
100
+ autoResize?: boolean;
80
101
  /**
81
102
  * Sandbox flags appended to the safe default (`allow-scripts`). Every
82
103
  * entry widens what agent-generated HTML may do — `allow-same-origin`
@@ -140,6 +161,7 @@ export function GuueyView(props: GuueyViewProps): ReactNode {
140
161
  const {
141
162
  mount,
142
163
  sandboxPageUrl,
164
+ autoResize,
143
165
  dangerouslyAddSandboxFlags,
144
166
  allow,
145
167
  title,
@@ -151,15 +173,19 @@ export function GuueyView(props: GuueyViewProps): ReactNode {
151
173
  } = props;
152
174
  const frameRef = useRef<HTMLIFrameElement>(null);
153
175
  const [phase, setPhase] = useState<ViewHostPhase>("negotiating");
176
+ // The view's own size report, applied only under `autoResize`.
177
+ const [reportedHeight, setReportedHeight] = useState<number | undefined>(undefined);
154
178
  const html = viewDocumentHtml(mount.resource);
155
179
 
156
180
  // Vet the sandbox page once per URL. Same-origin is REFUSED (the widget's
157
181
  // ResourceMount precedent, generalized): the whole point of the page is
158
182
  // being a different origin — same-origin would hand the relay page (and
159
183
  // through `allow-same-origin`, everything it can reach) the embedder's
160
- // own origin.
184
+ // own origin. `null` — page mode required but unconfigured — refuses the
185
+ // same way: srcdoc is never a silent fallback for a confinement posture.
161
186
  const sandboxPage: URL | "refused" | undefined = useMemo(() => {
162
187
  if (sandboxPageUrl === undefined) return undefined;
188
+ if (sandboxPageUrl === null) return "refused";
163
189
  let url: URL;
164
190
  try {
165
191
  url = new URL(sandboxPageUrl);
@@ -174,14 +200,15 @@ export function GuueyView(props: GuueyViewProps): ReactNode {
174
200
  // The attachment is keyed to the mounted DOCUMENT, not to every render's
175
201
  // fresh callback identities — host config rides a ref so the effect's
176
202
  // dependency list is honestly just the document identity.
177
- const latest = useRef({ hostConfig, onPhaseChange, dangerouslyAddSandboxFlags });
178
- latest.current = { hostConfig, onPhaseChange, dangerouslyAddSandboxFlags };
203
+ const latest = useRef({ hostConfig, onPhaseChange, dangerouslyAddSandboxFlags, autoResize });
204
+ latest.current = { hostConfig, onPhaseChange, dangerouslyAddSandboxFlags, autoResize };
179
205
 
180
206
  useEffect(() => {
181
207
  // Keyed to the same identity the frame is (the resource uri): a new
182
208
  // document boots fresh, and the previous negotiation's phase must not
183
- // paper over it.
209
+ // paper over it — nor must the previous document's reported size.
184
210
  setPhase("negotiating");
211
+ setReportedHeight(undefined);
185
212
  const frame = frameRef.current;
186
213
  if (frame === null || html === undefined) return;
187
214
  if (sandboxPageUrl !== undefined && page === undefined) return; // refused config — nothing mounts
@@ -193,6 +220,12 @@ export function GuueyView(props: GuueyViewProps): ReactNode {
193
220
  setPhase(next);
194
221
  latest.current.onPhaseChange?.(next);
195
222
  },
223
+ onSizeChanged: (size) => {
224
+ if (latest.current.autoResize === true && size.height !== undefined) {
225
+ setReportedHeight(size.height);
226
+ }
227
+ latest.current.hostConfig.onSizeChanged?.(size);
228
+ },
196
229
  });
197
230
  if (page === undefined) return detachHost;
198
231
  // Two-origin mode: the page announces readiness, the document is
@@ -226,12 +259,17 @@ export function GuueyView(props: GuueyViewProps): ReactNode {
226
259
  }
227
260
 
228
261
  if (sandboxPageUrl !== undefined && page === undefined) {
229
- // A malformed or SAME-ORIGIN sandbox page is a configuration state, not
230
- // a property of the card — refused, labeled, never mounted.
262
+ // A missing (null), malformed, or SAME-ORIGIN sandbox page is a
263
+ // configuration state, not a property of the card — refused, labeled,
264
+ // never mounted, and never silently downgraded to srcdoc. The copy
265
+ // names the configuration cause (an operator can act on it) without
266
+ // ever printing the offending URL.
231
267
  return (
232
268
  <div className={className} style={{ position: "relative", ...style }}>
233
269
  <p role="alert" style={{ ...statusLineStyle, opacity: 1, pointerEvents: "auto" }}>
234
- Interactive view unavailable — the sandbox page is not usable from this origin.
270
+ {sandboxPageUrl === null
271
+ ? "Interactive view unavailable — no sandbox page is configured."
272
+ : "Interactive view unavailable — the sandbox page is not usable from this origin."}
235
273
  </p>
236
274
  </div>
237
275
  );
@@ -260,7 +298,15 @@ export function GuueyView(props: GuueyViewProps): ReactNode {
260
298
  : ["allow-scripts", ...(dangerouslyAddSandboxFlags ?? [])].join(" ")
261
299
  }
262
300
  allow={allow ?? "clipboard-write"}
263
- style={{ display: "block", width: "100%", height: "100%", border: 0 }}
301
+ // Under `autoResize`, the view's own height report wins over the
302
+ // fill-the-container default (width stays the container's — a
303
+ // transcript column owns its width).
304
+ style={{
305
+ display: "block",
306
+ width: "100%",
307
+ height: autoResize === true && reportedHeight !== undefined ? reportedHeight : "100%",
308
+ border: 0,
309
+ }}
264
310
  />
265
311
  {renderStatus !== undefined ? renderStatus(phase) : defaultStatus(phase, mount.channel)}
266
312
  </div>
@@ -34,11 +34,13 @@ import {
34
34
  INITIALIZE_METHOD,
35
35
  LATEST_PROTOCOL_VERSION,
36
36
  RESOURCE_TEARDOWN_METHOD,
37
+ SIZE_CHANGED_METHOD,
37
38
  type McpUiHostCapabilities,
38
39
  type McpUiHostContext,
39
40
  type McpUiInitializeResult,
40
41
  } from "@modelcontextprotocol/ext-apps";
41
42
  import type { McpToolStructuredContent } from "./action.js";
43
+ import type { McpResourceReadResult } from "./reader.js";
42
44
 
43
45
  /**
44
46
  * The host identity in the initialize result — structurally the spec's
@@ -56,6 +58,23 @@ const METHOD_NOT_SUPPORTED = -32601;
56
58
  /** The standard MCP method a view uses to reach host-proxied tools. */
57
59
  export const TOOLS_CALL_METHOD = "tools/call";
58
60
 
61
+ /**
62
+ * The standard MCP method a view uses to read host-proxied resources —
63
+ * `ReadResourceRequest` in the spec's App→Host request union
64
+ * (`@modelcontextprotocol/ext-apps` `AppRequest`). A local constant, same
65
+ * as {@link TOOLS_CALL_METHOD}: the string is MCP-core vocabulary the
66
+ * ext-apps root does not re-export, and this package deliberately carries
67
+ * no `@modelcontextprotocol/sdk` dependency.
68
+ */
69
+ export const RESOURCES_READ_METHOD = "resources/read";
70
+
71
+ /**
72
+ * MCP's `Resource not found` JSON-RPC code — the one answer for a miss, a
73
+ * transport deny, AND a relay failure (deny == miss: the reader discipline,
74
+ * `reader.ts` — the view gets no oracle for which locators resolve).
75
+ */
76
+ const RESOURCE_NOT_FOUND = -32002;
77
+
59
78
  /** A JSON-RPC id as the wire allows it. */
60
79
  export type ViewRequestId = number | string;
61
80
 
@@ -117,6 +136,33 @@ export type ViewHostEffect =
117
136
  id: ViewRequestId;
118
137
  name: string;
119
138
  arguments?: McpToolStructuredContent;
139
+ }
140
+ | {
141
+ /**
142
+ * A `resources/read` the config accepted for relaying (spec surface:
143
+ * `ReadResourceRequest` rides the App→Host union, and the matching
144
+ * advertisement is `hostCapabilities.serverResources`). The glue runs
145
+ * the read hook and posts {@link resourceReadResponse}. Only emitted
146
+ * when {@link ViewHostBehavior.resourceRelay} is true — unwired, the
147
+ * machine refuses in-band like every other unsupported request.
148
+ */
149
+ kind: "relay-resource-read";
150
+ id: ViewRequestId;
151
+ uri: string;
152
+ }
153
+ | {
154
+ /**
155
+ * The view reported its content size (`ui/notifications/size-changed`
156
+ * — spec notification, App → Host). At least one of the two fields is
157
+ * a finite number; a notification carrying neither is consumed
158
+ * silently instead. The glue forwards this to the embedder
159
+ * ({@link AttachViewHostConfig.onSizeChanged} in `view-host.ts`) —
160
+ * whether/how to resize the frame is the embedder's layout decision,
161
+ * never the machine's.
162
+ */
163
+ kind: "size-changed";
164
+ width?: number;
165
+ height?: number;
120
166
  };
121
167
 
122
168
  /**
@@ -138,6 +184,8 @@ export interface ViewHostBehavior {
138
184
  hostContext: McpUiHostContext;
139
185
  /** Whether a `tools/call` relay hook is wired (see `view-host.ts`). */
140
186
  toolRelay: boolean;
187
+ /** Whether a `resources/read` relay hook is wired (see `view-host.ts`). */
188
+ resourceRelay: boolean;
141
189
  }
142
190
 
143
191
  /** The result of feeding one inbound frame (or the timeout) to the machine. */
@@ -205,6 +253,26 @@ export function toolCallResponse(
205
253
  return { jsonrpc: "2.0", id, result };
206
254
  }
207
255
 
256
+ /**
257
+ * Build the in-band response for a relayed `resources/read`. An entry
258
+ * becomes the spec's `ReadResourceResult` (`contents: [entry]`); `undefined`
259
+ * — a miss, a deny, or a relay failure alike — becomes the one
260
+ * `Resource not found` error (deny == miss, {@link RESOURCE_NOT_FOUND}).
261
+ */
262
+ export function resourceReadResponse(
263
+ id: ViewRequestId,
264
+ entry: McpResourceReadResult | undefined,
265
+ ): ViewHostOutbound {
266
+ if (entry === undefined) {
267
+ return {
268
+ jsonrpc: "2.0",
269
+ id,
270
+ error: { code: RESOURCE_NOT_FOUND, message: "resource unavailable" },
271
+ };
272
+ }
273
+ return { jsonrpc: "2.0", id, result: { contents: [entry] } };
274
+ }
275
+
208
276
  /**
209
277
  * The spec-mannered farewell a detaching host posts (`ui/resource-teardown`).
210
278
  * Sent WITHOUT an id — a host that is tearing the frame down cannot await a
@@ -237,10 +305,29 @@ export function viewHostReceive(
237
305
  if (req === undefined) return { state, effects: [] };
238
306
 
239
307
  if (req.id === undefined) {
240
- // A notification. Track the one the handshake defines; consume the rest.
308
+ // A notification. Track the one the handshake defines, surface the one
309
+ // the embedder may act on; consume the rest.
241
310
  if (req.method === "ui/notifications/initialized" && !state.initializedSeen) {
242
311
  return { state: { ...state, initializedSeen: true }, effects: [] };
243
312
  }
313
+ if (req.method === SIZE_CHANGED_METHOD) {
314
+ const width = req.params?.["width"];
315
+ const height = req.params?.["height"];
316
+ const validWidth = typeof width === "number" && Number.isFinite(width);
317
+ const validHeight = typeof height === "number" && Number.isFinite(height);
318
+ if (validWidth || validHeight) {
319
+ return {
320
+ state,
321
+ effects: [
322
+ {
323
+ kind: "size-changed",
324
+ ...(validWidth ? { width } : {}),
325
+ ...(validHeight ? { height } : {}),
326
+ },
327
+ ],
328
+ };
329
+ }
330
+ }
244
331
  return { state, effects: [] };
245
332
  }
246
333
 
@@ -271,6 +358,19 @@ export function viewHostReceive(
271
358
  // fall through: a nameless tools/call is not a call we can relay.
272
359
  }
273
360
 
361
+ if (req.method === RESOURCES_READ_METHOD && behavior.resourceRelay) {
362
+ const uri = req.params?.["uri"];
363
+ if (typeof uri === "string") {
364
+ return { state, effects: [{ kind: "relay-resource-read", id: req.id, uri }] };
365
+ }
366
+ // fall through: a uri-less read is not a read we can relay.
367
+ }
368
+
369
+ const answered = [
370
+ INITIALIZE_METHOD,
371
+ ...(behavior.toolRelay ? [TOOLS_CALL_METHOD] : []),
372
+ ...(behavior.resourceRelay ? [RESOURCES_READ_METHOD] : []),
373
+ ];
274
374
  return {
275
375
  state,
276
376
  effects: [
@@ -281,7 +381,7 @@ export function viewHostReceive(
281
381
  id: req.id,
282
382
  error: {
283
383
  code: METHOD_NOT_SUPPORTED,
284
- message: `method_not_supported: ${req.method} — this host answers ${INITIALIZE_METHOD}${behavior.toolRelay ? ` and ${TOOLS_CALL_METHOD}` : ""} only`,
384
+ message: `method_not_supported: ${req.method} — this host answers ${answered.join(", ")} only`,
285
385
  },
286
386
  },
287
387
  },
package/src/view-host.ts CHANGED
@@ -29,6 +29,7 @@
29
29
  */
30
30
  import {
31
31
  initialViewHostState,
32
+ resourceReadResponse,
32
33
  teardownMessage,
33
34
  toolCallResponse,
34
35
  viewHostElapsed,
@@ -38,6 +39,7 @@ import {
38
39
  type ViewHostPhase,
39
40
  type ViewHostState,
40
41
  } from "./view-host-protocol.js";
42
+ import type { McpResourceReadResult } from "./reader.js";
41
43
  import {
42
44
  unavailableToolCallResult,
43
45
  type McpToolCallResult,
@@ -107,6 +109,25 @@ export interface AttachViewHostConfig {
107
109
  * `<GuueyView>` fills it from the mount automatically.
108
110
  */
109
111
  resourceUri?: string;
112
+ /**
113
+ * The `resources/read` relay — a PRIVILEGE boundary like
114
+ * {@link onCallTool}, default off: with no hook, the machine refuses
115
+ * `resources/read` in-band and advertises no `serverResources`. The hook
116
+ * is structurally the SAME transport `createMcpUiResourceReader`
117
+ * assembles over ({@link CreateMcpUiResourceReaderDeps.readResource}) —
118
+ * a host with a locator reader wires the identical function here. Trust
119
+ * rules ride the reader discipline (`reader.ts`): enforcement lives
120
+ * INSIDE the transport; a miss, a deny, and a throw all answer the view
121
+ * with the one `Resource not found` error (deny == miss — no oracle).
122
+ */
123
+ onReadResource?: (uri: string) => Promise<McpResourceReadResult | undefined>;
124
+ /**
125
+ * The view reported its content size (`ui/notifications/size-changed` —
126
+ * spec notification). Whether and how to resize the frame is the
127
+ * embedder's layout decision; `<GuueyView autoResize>` is one wiring of
128
+ * exactly this callback.
129
+ */
130
+ onSizeChanged?: (size: { width?: number; height?: number }) => void;
110
131
  /** Observe phase transitions (see {@link ViewHostPhase}). */
111
132
  onPhaseChange?: (phase: ViewHostPhase) => void;
112
133
  /**
@@ -136,14 +157,37 @@ function hostContextFor(frame: ViewFrameLike, config: AttachViewHostConfig): Mcp
136
157
 
137
158
  function behaviorFor(frame: ViewFrameLike, config: AttachViewHostConfig): ViewHostBehavior {
138
159
  const relayWired = config.onCallTool !== undefined && config.resourceUri !== undefined;
160
+ const readWired = config.onReadResource !== undefined;
139
161
  return {
140
162
  hostInfo: config.hostInfo ?? DEFAULT_HOST_INFO,
141
163
  hostCapabilities: {
164
+ // A wired relay IS the implementation — advertise it; an explicit
165
+ // hostCapabilities entry still wins (the serverTools precedent).
142
166
  ...(relayWired ? { serverTools: {} } : {}),
167
+ ...(readWired ? { serverResources: {} } : {}),
143
168
  ...config.hostCapabilities,
144
169
  },
145
170
  hostContext: hostContextFor(frame, config),
146
171
  toolRelay: relayWired,
172
+ resourceRelay: readWired,
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Re-narrow a read hook's answer at the trust boundary — hooks are embedder
178
+ * code (possibly plain JS), and the wire entry the view receives must be a
179
+ * real `contents[]` entry: `uri` required, a string payload arm required
180
+ * (a payload-less entry is a miss — the `createMcpUiResourceReader`
181
+ * discipline, applied to the WIRE entry rather than the mountable payload).
182
+ */
183
+ function narrowReadEntry(entry: McpResourceReadResult | undefined): McpResourceReadResult | undefined {
184
+ if (entry === undefined || typeof entry.uri !== "string") return undefined;
185
+ if (typeof entry.text !== "string" && typeof entry.blob !== "string") return undefined;
186
+ return {
187
+ uri: entry.uri,
188
+ ...(typeof entry.mimeType === "string" ? { mimeType: entry.mimeType } : {}),
189
+ ...(typeof entry.text === "string" ? { text: entry.text } : {}),
190
+ ...(typeof entry.blob === "string" ? { blob: entry.blob } : {}),
147
191
  };
148
192
  }
149
193
 
@@ -183,13 +227,31 @@ export function attachViewHost(frame: ViewFrameLike, config: AttachViewHostConfi
183
227
  );
184
228
  };
185
229
 
230
+ const relayRead = (id: number | string, uri: string): void => {
231
+ const { onReadResource } = config;
232
+ if (onReadResource === undefined) return; // machine-guarded invariant, as with `relay`
233
+ onReadResource(uri).then(
234
+ (entry) => post(resourceReadResponse(id, narrowReadEntry(entry))),
235
+ // A throwing hook still owes the view an answer — the same not-found
236
+ // the reader discipline gives a deny (deny == miss), never a hang.
237
+ () => post(resourceReadResponse(id, undefined)),
238
+ );
239
+ };
240
+
186
241
  const onMessage = (event: { data: unknown; source: unknown }): void => {
187
242
  if (frame.contentWindow === null || event.source !== frame.contentWindow) return;
188
243
  const { state: next, effects } = viewHostReceive(state, behaviorFor(frame, config), event.data);
189
244
  setState(next);
190
245
  for (const effect of effects) {
191
246
  if (effect.kind === "respond") post(effect.message);
192
- else relay(effect.id, effect.name, effect.arguments);
247
+ else if (effect.kind === "relay-tool-call") relay(effect.id, effect.name, effect.arguments);
248
+ else if (effect.kind === "relay-resource-read") relayRead(effect.id, effect.uri);
249
+ else {
250
+ config.onSizeChanged?.({
251
+ ...(effect.width !== undefined ? { width: effect.width } : {}),
252
+ ...(effect.height !== undefined ? { height: effect.height } : {}),
253
+ });
254
+ }
193
255
  }
194
256
  };
195
257