Skip to content

Commit 2dd3ba8

Browse files
committed
More symmetrie with basic host
1 parent 5955a50 commit 2dd3ba8

9 files changed

Lines changed: 39 additions & 12 deletions

File tree

examples/dotnet-angular-host/frontend/public/sandbox.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
<!-- Copy of examples/basic-host/sandbox.html -->
2+
13
<!doctype html>
24
<html>
35
<head>

examples/dotnet-angular-host/frontend/src/app/components/app-iframe-panel/app-iframe-panel.html renamed to examples/dotnet-angular-host/frontend/src/app/components/iframe-panel/iframe-panel.html

File renamed without changes.

examples/dotnet-angular-host/frontend/src/app/components/app-iframe-panel/app-iframe-panel.scss renamed to examples/dotnet-angular-host/frontend/src/app/components/iframe-panel/iframe-panel.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
iframe {
55
width: 100%;
6-
height: 100px;
6+
height: 200px;
77
box-sizing: border-box;
88
border: 3px dashed var(--color-border);
99
border-radius: 4px;

examples/dotnet-angular-host/frontend/src/app/components/app-iframe-panel/app-iframe-panel.ts renamed to examples/dotnet-angular-host/frontend/src/app/components/iframe-panel/iframe-panel.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ import { CollapsiblePanel } from '../collapsible-panel/collapsible-panel';
2424

2525
@Component({
2626
selector: 'app-iframe-panel',
27-
templateUrl: './app-iframe-panel.html',
28-
styleUrl: './app-iframe-panel.scss',
27+
templateUrl: './iframe-panel.html',
28+
styleUrl: './iframe-panel.scss',
2929
imports: [CollapsiblePanel],
3030
})
31-
export class AppIframePanel implements AfterViewInit {
31+
export class IframePanel implements AfterViewInit {
3232
toolCallInfo = input.required<Required<ToolCallInfo>>();
3333
isDestroying = input(false);
3434

examples/dotnet-angular-host/frontend/src/app/components/tool-call-info-panel/tool-call-info-panel.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Component, computed, input, output, signal } from '@angular/core';
22
import { hasAppHtml, type ToolCallInfo } from '../../implementation';
33
import { CollapsiblePanel } from '../collapsible-panel/collapsible-panel';
4-
import { AppIframePanel } from '../app-iframe-panel/app-iframe-panel';
4+
import { IframePanel } from '../iframe-panel/iframe-panel';
55
import { ToolResultPanel } from '../tool-result-panel/tool-result-panel';
66

77
export type ToolCallEntry = ToolCallInfo & { id: number };
@@ -10,7 +10,7 @@ export type ToolCallEntry = ToolCallInfo & { id: number };
1010
selector: 'app-tool-call-info-panel',
1111
templateUrl: './tool-call-info-panel.html',
1212
styleUrl: './tool-call-info-panel.scss',
13-
imports: [CollapsiblePanel, AppIframePanel, ToolResultPanel],
13+
imports: [CollapsiblePanel, IframePanel, ToolResultPanel],
1414
})
1515
export class ToolCallInfoPanel {
1616
toolCallInfo = input.required<ToolCallEntry>();

examples/dotnet-angular-host/frontend/src/app/host-styles.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
// Copy of examples/basic-host/src/host-styles.ts
2+
13
/**
2-
* MCP style variables for the dotnet-angular-host example.
4+
* MCP style variables for the basic-host example.
35
* These are passed to apps via hostContext.styles.variables.
46
*/
57
import type { McpUiStyles } from '@modelcontextprotocol/ext-apps';

examples/dotnet-angular-host/frontend/src/app/implementation.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Copy of examples/basic-host/src/implementation.ts
2+
13
import {
24
RESOURCE_MIME_TYPE,
35
getToolUiResourceUri,
@@ -318,7 +320,10 @@ export function newAppBridge(
318320

319321
// Per spec, the host SHOULD notify the view when container dimensions
320322
// change. A ResizeObserver on the iframe covers window resize, layout
321-
// shifts, and the inline↔fullscreen panel toggle.
323+
// shifts, and the inline↔fullscreen panel toggle (which React applies
324+
// a tick after onrequestdisplaymode returns — sending containerDimensions
325+
// alongside displayMode there would race the layout). Height stays
326+
// flexible (maxHeight) so the view can keep driving it via sendSizeChanged.
322327
const iframeResizeObserver = new ResizeObserver(([entry]) => {
323328
const width = Math.round(entry.contentRect.width);
324329
if (width > 0) {
@@ -366,16 +371,26 @@ export function newAppBridge(
366371
};
367372

368373
appBridge.onsizechange = async ({ width, height }) => {
374+
// The MCP App has requested a `width` and `height`, but if
375+
// `box-sizing: border-box` is applied to the outer iframe element, then we
376+
// must add border thickness to `width` and `height` to compute the actual
377+
// necessary width and height (in order to prevent a resize feedback loop).
369378
const style = getComputedStyle(iframe);
370379
const isBorderBox = style.boxSizing === 'border-box';
371380

381+
// Animate the change for a smooth transition.
372382
const from: Keyframe = {};
373383
const to: Keyframe = {};
374384

375385
if (width !== undefined) {
376386
if (isBorderBox) {
377387
width += parseFloat(style.borderLeftWidth) + parseFloat(style.borderRightWidth);
378388
}
389+
// Use min-width instead of width to allow responsive growing.
390+
// With auto-resize (the default), the app reports its minimum content
391+
// width; we honor that as a floor but allow the iframe to expand when
392+
// the host layout allows. And we use `min(..., 100%)` so that the iframe
393+
// shrinks with its container.
379394
from['minWidth'] = `${iframe.offsetWidth}px`;
380395
iframe.style.minWidth = to['minWidth'] = `min(${width}px, 100%)`;
381396
}
@@ -394,7 +409,11 @@ export function newAppBridge(
394409
appBridge.onrequestdisplaymode = async (params) => {
395410
log.info('Display mode request from MCP App:', params);
396411
const newMode = params.mode === 'fullscreen' ? 'fullscreen' : 'inline';
397-
appBridge.sendHostContextChange({ displayMode: newMode });
412+
// Update host context and notify the app
413+
appBridge.sendHostContextChange({
414+
displayMode: newMode,
415+
});
416+
// Notify the host UI (via callback)
398417
callbacks?.onDisplayModeChange?.(newMode);
399418
return { mode: newMode };
400419
};

examples/dotnet-angular-host/frontend/src/app/theme.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
// Copy of examples/basic-host/src/theme.ts
2+
13
/**
2-
* Simple theme manager for the dotnet-angular-host example.
4+
* Simple theme manager for the basic-host example.
35
* Manages light/dark theme state and notifies listeners.
46
*/
57

examples/dotnet-angular-host/frontend/src/sandbox.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Copy of examples/basic-host/src/sandbox.ts
2+
13
import type {
24
McpUiSandboxProxyReadyNotification,
35
McpUiSandboxResourceReadyNotification,
@@ -45,7 +47,7 @@ try {
4547
// content. Per the specification, the Host and the Sandbox MUST have different
4648
// origins.
4749
const inner = document.createElement('iframe');
48-
inner.setAttribute('style', 'width:100%; height:100%; border:none;');
50+
inner.style = 'width:100%; height:100%; border:none;';
4951
inner.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms');
5052
// Note: allow attribute is set later when receiving sandbox-resource-ready notification
5153
// based on the permissions requested by the app
@@ -69,7 +71,7 @@ const PROXY_READY_NOTIFICATION: McpUiSandboxProxyReadyNotification['method'] =
6971
// intercepted here (not relayed) because the Sandbox uses it to configure and
7072
// load the inner iframe with the view HTML content.
7173
//
72-
// Security: CSP is enforced via HTTP headers on sandbox.html (set by the backend
74+
// Security: CSP is enforced via HTTP headers on sandbox.html (set by serve.ts
7375
// based on ?csp= query param). This is tamper-proof unlike meta tags.
7476

7577
window.addEventListener('message', async (event) => {

0 commit comments

Comments
 (0)