Skip to content

Conversation

@ochafik
Copy link
Contributor

@ochafik ochafik commented Nov 21, 2025

AppRenderer is a renderer for tool calls that return MCP Apps, adapted from this example to accept an optional MCP-UI onUIAction callback.

Apps can be created with the SDK at https://github.com/modelcontextprotocol/ext-apps

More details on SEP-1865

Requires updating MCP TS SDK from 1.11 to 1.22: sent separately as #148, merged here

cc/ @idosal @liady @antonpk1

@ochafik ochafik marked this pull request as ready for review November 24, 2025 19:51
export { isUIResource } from './utils/isUIResource';

// MCP-UI Templated Tool Call Renderer
export { UITemplatedToolCallRenderer, type UITemplatedToolCallRendererProps } from './components/UITemplatedToolCallRenderer';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export { UITemplatedToolCallRenderer, type UITemplatedToolCallRendererProps } from './components/UITemplatedToolCallRenderer';
export { AppRenderer, type AppRendererProps } from './components/AppRenderer';';

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed in e0c4421 — replaced the broken UITemplatedToolCallRenderer with AppRenderer export.

sandboxProxyUrl: URL;

/** MCP client connected to the server providing the tool */
client: Client;
Copy link

@infoxicator infoxicator Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ochafik this is convenient so we get the resource automatically and all the logic is encapsulated in the renderer, but in my case passing the entire client instance is tricky. What you think about having an additional mode where the resource (or its html) can be passed to the renderer directly and it is up to the client how to get that?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@infoxicator @ochafik Can we extract this component from AppRenderer so we'll export both the bare-bones component and the higher-level renderer on top of it?
I can take a stab at it later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @ochafik, we built our own renderer in MCPJam to provide initial support of MCP Apps, this renderer like @infoxicator points out, we pass HTML directly rather than requiring an MCP client instance (This pattern works well when the MCP client lives in a different context and ours lives in the server). Happy to discuss any of these if useful!

Copy link
Collaborator

@liady liady Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chelojimenez yeah, it's much simpler if the host can extract the HTML itself and pass it, and I agree that in your case it's even the only way (since the rendering context doesn't have an access to the client instance).
Note that @ochafik 's usage here is much wider - since this implementation relies on the AppBridge (from the ext-apps repo) - which needs the client in order to proxy MCP requests.
In the MCPJam implementation this is not needed - mcp-apps-renderer actually doesn't rely on MCP at all - and simply parses messages according to the schema. (It's a good renderer btw, even if it's not "generic" in that sense).

It's a good question regarding how we want to build the generic client side renderer here. @ochafik 's implementation encapsulates a lot of the functionality (including HTML resource mathcing) - but requires the client instance. On the other hand a more "low-level" renderer would leave more heavy-lifting to the host, and if we skip using AppBridge we might be missing the MCP re-use.

As @idosal is also working on the client side SDK, we'll probably need to align on the best abstraction here. I think that allowing (optionally) to pass the raw resource HTML instead of the entire client is a good, but we still need to solve for the usage of AppBridge that does require the client instance.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am experimenting using a facade / relay pattern to proxy the mcp events without having to pass the entire client. its working on postman but I will need some time to figure out how to make it fully reusable and also provide it as an alternative to passing the client

Copy link
Contributor Author

@ochafik ochafik Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(sorry for the noise, toying w/ updates to AppBridge to not require a Client / give users more flexibility)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making Client optional here: modelcontextprotocol/ext-apps#146

*/
export interface AppRendererProps {
/** URL to the sandbox proxy HTML that will host the tool UI iframe */
sandboxProxyUrl: URL;
Copy link
Collaborator

@idosal idosal Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should change it to a sandbox prop with an internal object for url. I imagine sandbox will have additional properties in the future (such as permissions). WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d12ddcc:

1. sandbox prop with object structure:

// Before
sandboxProxyUrl={new URL('...')}

// After
sandbox={{ url: new URL('...'), permissions?: string, csp?: McpUiResourceCsp }}

sandboxProxyUrl still works with a deprecation warning.

2. Extracted bare-bones component:

  • AppFrame — low-level, takes html directly + optional appBridge
  • AppRenderer — high-level, fetches resources, creates bridge internally

Both are exported from @mcp-ui/client.

if (
event.data &&
event.data.method ===
McpUiSandboxProxyReadyNotificationSchema.shape.method._def.value
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be .values[0]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed in e0c4421 — good catch on the Zod v4 API change!

@ochafik
Copy link
Contributor Author

ochafik commented Dec 8, 2025

(sorry for the delay, back on this!)

- Fix Zod v4 compatibility: .value → .values[0] (guru3s)
- Fix index.ts export: replace broken UITemplatedToolCallRenderer with AppRenderer
- Upgrade @modelcontextprotocol/sdk to ^1.23.0 to match ext-apps
- Use RESOURCE_URI_META_KEY from ext-apps instead of local constant
- Fix logging message type handling

Note: ext-apps dependency temporarily uses file: reference due to
git install issues with esbuild prepare script.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@aharvard
Copy link
Contributor

aharvard commented Dec 9, 2025

This is great! Looking forward to using it in Goose when ready.

- Add AppFrame: low-level component for rendering pre-fetched HTML
  - Takes html directly, optionally with pre-configured AppBridge
  - Supports simple callbacks (onOpenLink, onMessage, onSizeChange)
  - Forwards CSP metadata to sandbox proxy

- Refactor AppRenderer to use AppFrame internally
  - Add sandbox prop with SandboxConfig type (replaces sandboxProxyUrl)
  - Add optional html prop to skip resource fetching
  - Deprecate sandboxProxyUrl (still works with warning)
  - Use proper param types in callbacks (McpUiMessageRequest, etc.)

- Update to @modelcontextprotocol/ext-apps ^0.1.0
  - Use RESOURCE_MIME_TYPE from ext-apps
  - Import from /app-bridge subpath

- Export AppFrame, AppFrameProps, SandboxConfig from index.ts

Addresses PR comments:
- @idosal: Extract bare-bones component, sandbox prop with object
- @infoxicator, @chelojimenez, @liady: Optional HTML pass-through mode

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@ochafik
Copy link
Contributor Author

ochafik commented Dec 12, 2025

@guru3s ✅ Fixed in e0c4421 — good catch on the Zod v4 API change!

- Update @modelcontextprotocol/ext-apps to ochafik/app-bridge-setters branch
- This enables optional MCP client in AppBridge constructor
- Adds oncalltool, onlistresources, onreadresource, etc. setters for custom handlers
- Adds sendToolListChanged, sendResourceListChanged, sendPromptListChanged methods
@ochafik
Copy link
Contributor Author

ochafik commented Dec 12, 2025

Update: Setter-based MCP forwarding in AppBridge

Merged latest main and updated @modelcontextprotocol/ext-apps to use a new branch with a cleaner API design.

Changes in ext-apps (ochafik/app-bridge-setters)

Made Client optional in AppBridge constructor - hosts can now pass null and register handlers manually:

// Option 1: With client (automatic forwarding - existing behavior)
const bridge = new AppBridge(mcpClient, hostInfo, capabilities);

// Option 2: Without client (manual handlers)
const bridge = new AppBridge(null, hostInfo, capabilities);
bridge.oncalltool = async (params, extra) => { /* custom handling */ };
bridge.onlistresources = async (params, extra) => { /* custom handling */ };

New setter-based handlers for Guest UI → Host requests:

  • oncalltool - handle tools/call requests
  • onlistresources - handle resources/list requests
  • onlistresourcetemplates - handle resources/templates/list requests
  • onreadresource - handle resources/read requests
  • onlistprompts - handle prompts/list requests

New send methods for Host → Guest notifications:

  • sendToolListChanged()
  • sendResourceListChanged()
  • sendPromptListChanged()

Why this matters

The previous design required a full MCP Client and automatically wired up all forwarding. The new design enables:

  • Static HTML UIs with no MCP calls
  • Custom/filtered MCP proxying (e.g., caching, authorization, logging)
  • Multi-server aggregation
  • Testing without a real MCP client

Build/test status

  • ✅ All 120 client tests pass
  • ✅ Build succeeds

Resolved conflicts:
- sdks/typescript/client/package.json: kept ext-apps branch dependency
- sdks/typescript/client/src/components/UIResourceRendererWC.tsx: use import type
- sdks/typescript/client/src/components/__tests__/UIResourceRenderer.unmocked.test.tsx: use import type
- sdks/typescript/client/src/utils/processResource.ts: use import type
- examples/mcp-apps-demo/package.json: updated ext-apps dependency to our branch
Option 1 - Add request handler props to AppRendererProps:
- oncalltool: handle tools/call requests
- onlistresources: handle resources/list requests
- onlistresourcetemplates: handle resources/templates/list requests
- onreadresource: handle resources/read requests
- onlistprompts: handle prompts/list requests

Option 2 - Expose AppBridge via ref (AppRendererHandle):
- appBridge: direct access to AppBridge instance
- sendToolListChanged(): notify guest of tool list changes
- sendResourceListChanged(): notify guest of resource list changes
- sendPromptListChanged(): notify guest of prompt list changes

Option 3 - Re-export from index.ts:
- AppBridge: for creating custom bridges
- PostMessageTransport: for custom transport setups

Also:
- Make client prop nullable (required html when client is null)
- Export RequestHandlerExtra type for custom handler signatures
Breaking changes:
- AppFrame.appBridge is now required (was optional)
- Removed postMessage fallback from AppFrame

AppRenderer changes:
- Always creates AppBridge internally
- client prop can be null (requires html prop when null)
- Exposes ref handle with send methods (sendToolListChanged, etc.)
- Removed appBridge from ref handle (use AppFrame directly for full control)

New MCP request handler props on AppRenderer:
- oncalltool
- onlistresources
- onlistresourcetemplates
- onreadresource
- onlistprompts

New send methods on AppRendererHandle:
- sendToolListChanged
- sendResourceListChanged
- sendPromptListChanged
- sendToolInput
- sendToolInputPartial
- sendToolResult
- sendToolCancelled
- sendHostContextChange

Re-exports from @mcp-ui/client:
- AppBridge
- PostMessageTransport
Breaking changes:
- AppFrame.appBridge is now required (was optional)
- Removed postMessage fallback from AppFrame

AppRenderer changes:
- Always creates AppBridge internally
- client prop can be null (requires html prop when null)
- Exposes ref handle with send methods (sendToolListChanged, etc.)
- Removed appBridge from ref handle (use AppFrame directly for full control)

New MCP request handler props on AppRenderer:
- oncalltool
- onlistresources
- onlistresourcetemplates
- onreadresource
- onlistprompts

New send methods on AppRendererHandle:
- sendToolListChanged
- sendResourceListChanged
- sendPromptListChanged
- sendToolInput
- sendToolInputPartial
- sendToolResult
- sendToolCancelled
- sendHostContextChange

Re-exports from @mcp-ui/client:
- AppBridge
- PostMessageTransport
API changes:

AppRendererHandle (ref):
- Remove: sendToolInput, sendToolResult, sendToolInputPartial, sendToolCancelled
- Add: sendResourceTeardown (for cleanup before unmounting)
- Keep: sendToolListChanged, sendResourceListChanged, sendPromptListChanged

AppRendererProps:
- Add: toolInputPartial (for streaming partial input)
- Add: toolCancelled (boolean flag for cancellation)
- Deprecate: onUIAction (use onopenlink, onmessage, onloggingmessage instead)

AppFrameProps callback naming (camelCase):
- onSizeChanged (was onSizeChange)
- onLoggingMessage
- onInitialized
AppRendererProps:
- onOpenLink (was onopenlink)
- onMessage (was onmessage)
- onLoggingMessage (was onloggingmessage)
- onSizeChanged (was onsizechange)
- onError (was onerror)
- onCallTool (was oncalltool)
- onListResources (was onlistresources)
- onListResourceTemplates (was onlistresourcetemplates)
- onReadResource (was onreadresource)
- onListPrompts (was onlistprompts)

AppFrameProps:
- onSizeChanged
- onLoggingMessage
- onInitialized
- onError (was onerror)
New test coverage:
- hostContext prop (setHostContext calls)
- toolInputPartial prop (sendToolInputPartial calls)
- toolCancelled prop (sendToolCancelled calls)
- ref methods: sendToolListChanged, sendResourceListChanged, sendPromptListChanged, sendResourceTeardown
- MCP request handler props: onCallTool, onListResources, onListResourceTemplates, onReadResource, onListPrompts
- callback props forwarding: onSizeChanged, onError
- null client behavior (with/without html prop)
- Update @modelcontextprotocol/ext-apps to 4653156 (latest on ochafik/app-bridge-setters)
- Fix hostContext test to use proper McpUiTheme literal types ('dark' | 'light')
- Fix toolInputPartial test to match McpUiToolInputPartialNotification params shape
@ochafik ochafik force-pushed the mcp-apps branch 2 times, most recently from 296aab1 to a4ec2f0 Compare December 13, 2025 12:08
- Create eslint.config.mjs for ESLint v9 flat config format
- Disable @typescript-eslint/no-empty-object-type (pre-existing issues)
- Remove unused imports from AppRenderer.tsx
- Clean up AppRenderer.test.tsx (remove unused helper)
The ochafik/app-bridge-setters branch has been merged to main.

// Read HTML content
console.log(`[AppRenderer] Reading resource HTML from: ${uri}`);
const htmlContent = await readToolUiResourceHtml(client, { uri });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ochafik I am considering using the onReadResource prop here instead of requiring the entire client.

Passing the HTML directly works but it would be nice if this component contains the logic for getting the getToolUiResourceUri and readToolUiResourceHtml as well without the need for the client

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in fece80e! The component now supports fetching HTML without a Client by providing toolResourceUri + onReadResource:

<AppRenderer
  toolName="my-tool"
  toolResourceUri="ui://my-server/my-tool"
  onReadResource={async ({ uri }) => myProxy.readResource({ uri })}
  onCallTool={async (params) => myProxy.callTool(params)}
/>

This enables decoupled architectures where the MCP client lives in a different context.

Copy link

@infoxicator infoxicator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image Working great so far @ochafik

One comment to make the decoupling of client complete and then we are mostly there 🙌

@infoxicator infoxicator mentioned this pull request Dec 15, 2025
6 tasks
@aharvard
Copy link
Contributor

@ochafik, I was testing AppRenderer in Goose and found this tiny nit modelcontextprotocol/ext-apps#156

*/
export interface AppRendererProps {
/** MCP client connected to the server providing the tool. Pass `null` to disable automatic MCP forwarding and use custom handlers instead. */
client: Client | null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ochafik small nit. We can mark the Client as optional? and check for undefined instead of having to pass null?

Copy link
Contributor Author

@ochafik ochafik Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in fece80e! Changed from client: Client | null to client?: Client

ochafik and others added 3 commits December 16, 2025 13:03
- @remote-dom/core 1.8.1 → 1.10.1
- @quilted/threads 3.1.3 → 3.3.1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Addresses PR feedback:
- Changed `client: Client | null` to `client?: Client` for cleaner API
- Added support for using `onReadResource` + `toolResourceUri` to fetch
  HTML without requiring the full MCP Client instance
- This enables decoupled architectures where the MCP client lives in a
  different context (e.g., server-side)

Usage without client:
```tsx
<AppRenderer
  toolName="my-tool"
  toolResourceUri="ui://my-server/my-tool"
  onReadResource={async ({ uri }) => myProxy.readResource({ uri })}
  onCallTool={async (params) => myProxy.callTool(params)}
/>
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Breaking changes in ext-apps v0.2.0:
- sendResourceTeardown() renamed to teardownResource() (deprecated alias exists)
- MCP SDK moved to peer dependency, requiring ^1.24.0
- Method renaming: sendOpenLink() → openLink() (not used in mcp-ui)

Updates:
- Update @modelcontextprotocol/ext-apps from github#main/^0.0.7 to ^0.2.0
- Update @modelcontextprotocol/sdk from ^1.22.0/^1.23.0 to ^1.24.0
- Rename AppRendererHandle.sendResourceTeardown to teardownResource
- Update tests to use new method name

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces React renderer components for MCP Apps, enabling hosts to render tool UIs in sandboxed iframes with bidirectional MCP communication. The implementation is adapted from the modelcontextprotocol/ext-apps example and adds support for the MCP Apps SEP-1865 protocol, including updating the MCP TypeScript SDK from 1.11 to 1.23.

Key changes:

  • Adds AppRenderer component for high-level tool UI rendering with automatic resource fetching and AppBridge management
  • Adds AppFrame component for low-level sandbox iframe rendering with pre-configured AppBridge
  • Adds utility functions for sandbox setup and resource reading (app-host-utils.ts)

Reviewed changes

Copilot reviewed 15 out of 19 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
sdks/typescript/client/src/components/AppRenderer.tsx New component providing high-level interface for rendering MCP tool UIs with automatic AppBridge setup, resource fetching, and MCP request handling
sdks/typescript/client/src/components/AppFrame.tsx New low-level component for rendering pre-fetched HTML in sandboxed iframe with AppBridge communication
sdks/typescript/client/src/utils/app-host-utils.ts New utilities for sandbox iframe setup, tool resource URI resolution, and HTML reading
sdks/typescript/client/src/components/__tests__/AppRenderer.test.tsx Comprehensive test suite for AppRenderer covering rendering, props, callbacks, and ref methods
sdks/typescript/client/src/components/__tests__/AppFrame.test.tsx Comprehensive test suite for AppFrame covering sandbox setup, bridge connection, and message handling
sdks/typescript/client/src/index.ts Exports new AppRenderer, AppFrame components and re-exports AppBridge types for public API
sdks/typescript/client/package.json Updates MCP SDK to 1.23.0, adds ext-apps dependency, and adds zod dependency
examples/mcp-apps-demo/package.json Updates ext-apps dependency to use GitHub main branch reference
eslint.config.mjs New ESLint flat config with TypeScript and React support, replaces legacy configuration
sdks/typescript/client/src/utils/processResource.ts Whitespace formatting fix (trailing whitespace removed)
sdks/typescript/client/src/remote-dom/iframe-bundle.ts Updates bundled remote-dom and quilted/threads libraries to newer versions
Various test files Formatting improvements (consistent indentation, whitespace cleanup)
Various component files Formatting improvements (trailing whitespace removal, consistent indentation)

iframe.style.height = '600px';
iframe.style.border = 'none';
iframe.style.backgroundColor = 'transparent';
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms');
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sandbox attribute is hardcoded to allow-scripts allow-same-origin allow-forms. While these permissions are necessary for the sandbox to function, the allow-same-origin permission combined with allow-scripts effectively removes the security sandbox. This is a known security concern. Consider documenting why this is necessary and if there are alternative approaches. Also, the sandbox configuration should ideally be configurable via the SandboxConfig interface to allow hosts to enforce stricter policies if needed.

Copilot uses AI. Check for mistakes.
Comment on lines 20 to 32
const onReady = new Promise<void>((resolve, _reject) => {
const initialListener = async (event: MessageEvent) => {
if (event.source === iframe.contentWindow) {
if (
event.data &&
event.data.method === McpUiSandboxProxyReadyNotificationSchema.shape.method._def.values[0]
) {
window.removeEventListener('message', initialListener);
resolve();
}
}
};
window.addEventListener('message', initialListener);
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _reject parameter in the Promise constructor is unused and should be prefixed with an underscore or removed if truly unused. Currently, the promise never rejects, which means the caller cannot handle setup failures. Consider adding error handling and rejection logic, or document why rejection is not needed.

Suggested change
const onReady = new Promise<void>((resolve, _reject) => {
const initialListener = async (event: MessageEvent) => {
if (event.source === iframe.contentWindow) {
if (
event.data &&
event.data.method === McpUiSandboxProxyReadyNotificationSchema.shape.method._def.values[0]
) {
window.removeEventListener('message', initialListener);
resolve();
}
}
};
window.addEventListener('message', initialListener);
const onReady = new Promise<void>((resolve, reject) => {
let settled = false;
const timeoutMs = 10000; // 10 seconds
const timeoutId = setTimeout(() => {
if (!settled) {
settled = true;
window.removeEventListener('message', initialListener);
iframe.removeEventListener('error', errorListener);
reject(new Error('Timed out waiting for sandbox proxy iframe to be ready'));
}
}, timeoutMs);
const initialListener = async (event: MessageEvent) => {
if (event.source === iframe.contentWindow) {
if (
event.data &&
event.data.method === McpUiSandboxProxyReadyNotificationSchema.shape.method._def.values[0]
) {
if (!settled) {
settled = true;
clearTimeout(timeoutId);
window.removeEventListener('message', initialListener);
iframe.removeEventListener('error', errorListener);
resolve();
}
}
}
};
const errorListener = () => {
if (!settled) {
settled = true;
clearTimeout(timeoutId);
window.removeEventListener('message', initialListener);
iframe.removeEventListener('error', errorListener);
reject(new Error('Failed to load sandbox proxy iframe'));
}
};
window.addEventListener('message', initialListener);
iframe.addEventListener('error', errorListener);

Copilot uses AI. Check for mistakes.
Comment on lines 20 to 25
const onReady = new Promise<void>((resolve, _reject) => {
const initialListener = async (event: MessageEvent) => {
if (event.source === iframe.contentWindow) {
if (
event.data &&
event.data.method === McpUiSandboxProxyReadyNotificationSchema.shape.method._def.values[0]
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessing array element [0] using bracket notation on a Zod schema definition is fragile. This assumes the schema structure and internal Zod implementation details. If the Zod schema shape changes in future versions, this code will break. Consider using a more robust approach, such as defining the expected message type as a constant or using Zod's type extraction methods.

Suggested change
const onReady = new Promise<void>((resolve, _reject) => {
const initialListener = async (event: MessageEvent) => {
if (event.source === iframe.contentWindow) {
if (
event.data &&
event.data.method === McpUiSandboxProxyReadyNotificationSchema.shape.method._def.values[0]
// Define the expected method string for the ready notification.
// This should match the value used in the schema definition.
const MCP_UI_SANDBOX_PROXY_READY_METHOD = 'mcp.ui.sandboxProxy.ready';
const onReady = new Promise<void>((resolve, _reject) => {
const initialListener = async (event: MessageEvent) => {
if (event.source === iframe.contentWindow) {
if (
event.data &&
event.data.method === MCP_UI_SANDBOX_PROXY_READY_METHOD

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +24
// MCP Apps renderers
export {
AppRenderer,
type AppRendererProps,
type AppRendererHandle,
type RequestHandlerExtra,
} from './components/AppRenderer';
export {
AppFrame,
type AppFrameProps,
type SandboxConfig,
type AppInfo,
} from './components/AppFrame';

// Re-export AppBridge, transport, and common types for advanced use cases
export {
AppBridge,
PostMessageTransport,
type McpUiHostContext,
} from '@modelcontextprotocol/ext-apps/app-bridge';
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new AppRenderer and AppFrame components are exported but lack documentation. These are significant new public APIs that should be documented similarly to the existing UIResourceRenderer component. Consider adding documentation pages explaining their purpose, usage examples, API reference, and how they relate to the MCP Apps ecosystem. The existing documentation structure in docs/src/guide/client/ would be a good place for these.

Copilot uses AI. Check for mistakes.
],
"dependencies": {
"@modelcontextprotocol/sdk": "^1.22.0",
"@modelcontextprotocol/ext-apps": "github:modelcontextprotocol/ext-apps#main",
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dependency @modelcontextprotocol/ext-apps is specified using a GitHub URL reference (github:modelcontextprotocol/ext-apps#main). This is not a stable versioning strategy and can lead to unpredictable behavior as the main branch changes. Consider using a specific commit hash, tag, or published npm version for reproducible builds.

Suggested change
"@modelcontextprotocol/ext-apps": "github:modelcontextprotocol/ext-apps#main",
"@modelcontextprotocol/ext-apps": "github:modelcontextprotocol/ext-apps#abcdef1234567890abcdef1234567890abcdef12",

Copilot uses AI. Check for mistakes.
Comment on lines +451 to +452
"Either 'html' prop, 'client', or ('toolResourceUri' + 'onReadResource') must be provided to fetch UI resource",
),
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling doesn't provide specific guidance when both client and html are missing but toolResourceUri is provided without onReadResource. The error message should be clearer about the required combinations. Consider mentioning that toolResourceUri requires either a client or an onReadResource callback.

Suggested change
"Either 'html' prop, 'client', or ('toolResourceUri' + 'onReadResource') must be provided to fetch UI resource",
),
"Unable to fetch UI resource: you must provide one of the following combinations:\n" +
" - 'html' prop (static HTML provided)\n" +
" - 'client' (to fetch the resource via MCP)\n" +
" - 'toolResourceUri' AND either 'client' or 'onReadResource' callback (to fetch the resource by URI)\n" +
"Note: Providing 'toolResourceUri' alone is not sufficient; you must also provide a 'client' or an 'onReadResource' callback to fetch the resource.",

Copilot uses AI. Check for mistakes.
let uri: string;
if (toolResourceUri) {
uri = toolResourceUri;
console.log(`[AppRenderer] Using provided resource URI: ${uri}`);
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Console.log statements are left in production code. These should either be removed, converted to a proper logging mechanism (with configurable log levels), or wrapped in a debug flag. Production code shouldn't have verbose console logs that can expose internal implementation details or clutter user consoles.

Copilot uses AI. Check for mistakes.
if (!mounted) return;

// Read HTML content - use client if available, otherwise use onReadResource callback
console.log(`[AppRenderer] Reading resource HTML from: ${uri}`);
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Console.log statements are left in production code. These should either be removed, converted to a proper logging mechanism (with configurable log levels), or wrapped in a debug flag. Production code shouldn't have verbose console logs.

Suggested change
console.log(`[AppRenderer] Reading resource HTML from: ${uri}`);

Copilot uses AI. Check for mistakes.
// Hook into initialization
appBridge.oninitialized = () => {
if (!mounted) return;
console.log('[AppFrame] App initialized');
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Console.log statements are left in production code. These should either be removed, converted to a proper logging mechanism (with configurable log levels), or wrapped in a debug flag.

Suggested change
console.log('[AppFrame] App initialized');
if (process.env.NODE_ENV !== 'production') {
console.log('[AppFrame] App initialized');
}

Copilot uses AI. Check for mistakes.
Comment on lines +209 to +235
console.log('[AppFrame] Sending HTML to sandbox');
await appBridge.sendSandboxResourceReady({
html,
csp: sandbox.csp,
});
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
onErrorRef.current?.(error);
}
};

sendHtml();
}, [bridgeConnected, html, appBridge, sandbox.csp]);

// Effect 3: Send tool input when ready
useEffect(() => {
if (bridgeConnected && iframeReady && toolInput) {
console.log('[AppFrame] Sending tool input:', toolInput);
appBridge.sendToolInput({ arguments: toolInput });
}
}, [appBridge, bridgeConnected, iframeReady, toolInput]);

// Effect 4: Send tool result when ready
useEffect(() => {
if (bridgeConnected && iframeReady && toolResult) {
console.log('[AppFrame] Sending tool result:', toolResult);
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Console.log statements are left in production code. These should either be removed, converted to a proper logging mechanism (with configurable log levels), or wrapped in a debug flag.

Copilot uses AI. Check for mistakes.
- Add "Host-Side Rendering" section to mcp-apps.md documenting:
  - AppRenderer component usage and props
  - Using without an MCP client (custom handlers or pre-fetched HTML)
  - AppFrame low-level component
  - Sandbox proxy requirements

- Fix promise rejection in setupSandboxProxyIframe:
  - Add 10s timeout for sandbox ready message
  - Add error listener for iframe load failures
  - Proper cleanup of event listeners

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@ochafik
Copy link
Contributor Author

ochafik commented Dec 16, 2025

Update: ext-apps v0.2.0 upgrade

Upgraded @modelcontextprotocol/ext-apps to ^0.2.0 and @modelcontextprotocol/sdk to ^1.24.0.

Changes in ext-apps v0.2.0

  1. Method renaming (with deprecated aliases):

    • sendResourceTeardown()teardownResource()
    • sendOpenLink()openLink() (not used in mcp-ui)
  2. MCP SDK moved to peer dependency - requires @modelcontextprotocol/sdk@^1.24.0

  3. Optional transport in connect() - can now call without parameters (auto-detects platform)

Changes in mcp-ui

  • Updated all packages to use ^0.2.0 instead of github:...#main
  • Renamed AppRendererHandle.sendResourceTeardownteardownResource to align with upstream
  • Updated MCP SDK to ^1.24.0 across all packages

Additional fixes

  • Added documentation for AppRenderer and AppFrame components in mcp-apps.md
  • Fixed promise rejection handling in setupSandboxProxyIframe (timeout + error listener)

All tests pass ✅

"@modelcontextprotocol/sdk": "^1.22.0",
"@modelcontextprotocol/ext-apps": "^0.2.0",
"@modelcontextprotocol/sdk": "^1.24.0",
"@mcp-ui/shared": "workspace:*",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ochafik was this change needed? it is causing a resolution issue when trying to install locally ➤ YN0001: │ Error: @mcp-ui/shared@workspace:*: Workspace not found (@mcp-ui/shared@workspace:*) removing this line fixes the error

Comment on lines +197 to +199
if (iframeRef.current && containerRef.current?.contains(iframeRef.current)) {
containerRef.current.removeChild(iframeRef.current);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (iframeRef.current && containerRef.current?.contains(iframeRef.current)) {
containerRef.current.removeChild(iframeRef.current);
}

@ochafik found a lifecycle problem. This code destroys the iframe when the component unmounts. but then the mounting process doesn't happen again (create iframe setHtml). I don't think that's what we want anyway?

The iframe should preserve the state when rerendering instead of destroying and recreating the iframe?

/** Sandbox configuration */
sandbox: SandboxConfig;

/** @deprecated Use `sandbox.url` instead */

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: There are no clients currently using this so I guess we can omit these deprecated methods?

- Resolved conflicts in eslint.config.mjs (using origin/main version)
- Resolved conflicts in package.json files (using latest dependency versions)
- Updated pnpm-lock.yaml after dependency resolution
@idosal
Copy link
Collaborator

idosal commented Dec 18, 2025

@ochafik - the eslint update on main caused conflicts with the PR and revealed a few issues. Fixed them all and merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants