-
Notifications
You must be signed in to change notification settings - Fork 278
React renderer for MCP Apps #147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
sdks/typescript/client/src/index.ts
Outdated
| export { isUIResource } from './utils/isUIResource'; | ||
|
|
||
| // MCP-UI Templated Tool Call Renderer | ||
| export { UITemplatedToolCallRenderer, type UITemplatedToolCallRendererProps } from './components/UITemplatedToolCallRenderer'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| export { UITemplatedToolCallRenderer, type UITemplatedToolCallRendererProps } from './components/UITemplatedToolCallRenderer'; | |
| export { AppRenderer, type AppRendererProps } from './components/AppRenderer';'; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, takeshtmldirectly + optionalappBridgeAppRenderer— 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 |
There was a problem hiding this comment.
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]
There was a problem hiding this comment.
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!
|
(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]>
|
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]>
- 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
Update: Setter-based MCP forwarding in AppBridgeMerged latest Changes in ext-apps (
|
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
296aab1 to
a4ec2f0
Compare
- 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 }); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
infoxicator
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Working great so far @ochafik
One comment to make the decoupling of client complete and then we are mostly there 🙌
|
@ochafik, I was testing |
| */ | ||
| 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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
- @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]>
There was a problem hiding this 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
AppRenderercomponent for high-level tool UI rendering with automatic resource fetching and AppBridge management - Adds
AppFramecomponent 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'); |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
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.
| 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); |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
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.
| 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); |
| 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] |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
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.
| 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 |
| // 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'; |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
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.
sdks/typescript/client/package.json
Outdated
| ], | ||
| "dependencies": { | ||
| "@modelcontextprotocol/sdk": "^1.22.0", | ||
| "@modelcontextprotocol/ext-apps": "github:modelcontextprotocol/ext-apps#main", |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
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.
| "@modelcontextprotocol/ext-apps": "github:modelcontextprotocol/ext-apps#main", | |
| "@modelcontextprotocol/ext-apps": "github:modelcontextprotocol/ext-apps#abcdef1234567890abcdef1234567890abcdef12", |
| "Either 'html' prop, 'client', or ('toolResourceUri' + 'onReadResource') must be provided to fetch UI resource", | ||
| ), |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
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.
| "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.", |
| let uri: string; | ||
| if (toolResourceUri) { | ||
| uri = toolResourceUri; | ||
| console.log(`[AppRenderer] Using provided resource URI: ${uri}`); |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
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.
| if (!mounted) return; | ||
|
|
||
| // Read HTML content - use client if available, otherwise use onReadResource callback | ||
| console.log(`[AppRenderer] Reading resource HTML from: ${uri}`); |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
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.
| console.log(`[AppRenderer] Reading resource HTML from: ${uri}`); |
| // Hook into initialization | ||
| appBridge.oninitialized = () => { | ||
| if (!mounted) return; | ||
| console.log('[AppFrame] App initialized'); |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
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.
| console.log('[AppFrame] App initialized'); | |
| if (process.env.NODE_ENV !== 'production') { | |
| console.log('[AppFrame] App initialized'); | |
| } |
| 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); |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
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.
- 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]>
Update: ext-apps v0.2.0 upgradeUpgraded Changes in ext-apps v0.2.0
Changes in mcp-ui
Additional fixes
All tests pass ✅ |
| "@modelcontextprotocol/sdk": "^1.22.0", | ||
| "@modelcontextprotocol/ext-apps": "^0.2.0", | ||
| "@modelcontextprotocol/sdk": "^1.24.0", | ||
| "@mcp-ui/shared": "workspace:*", |
There was a problem hiding this comment.
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
| if (iframeRef.current && containerRef.current?.contains(iframeRef.current)) { | ||
| containerRef.current.removeChild(iframeRef.current); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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 */ |
There was a problem hiding this comment.
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
|
@ochafik - the eslint update on |
AppRenderer is a renderer for tool calls that return MCP Apps, adapted from this example to accept an optional MCP-UI
onUIActioncallback.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