Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9184830
UITemplatedToolCallRendererProps for MCP Apps
ochafik Nov 21, 2025
73c2c0d
Upgrade MCP SDK to 1.22.0 (many (Embedded)Resource type fixes)
ochafik Nov 22, 2025
3b23ede
Update adapter.ts
ochafik Nov 22, 2025
5342f81
Update UIResourceRendererWC.test.tsx
ochafik Nov 22, 2025
6d07a4b
add missing client dep (vite-tsconfig-paths)
ochafik Nov 22, 2025
098d10e
Merge branch 'ochafik/upgrade' into mcp-apps
ochafik Nov 22, 2025
c55ef76
update to latest ext-apps example renderer
ochafik Nov 24, 2025
e0c4421
Sync with latest ext-apps, fix PR review comments
ochafik Dec 8, 2025
d12ddcc
Add AppFrame component, refactor AppRenderer, use ext-apps v0.1.0
ochafik Dec 12, 2025
8aa7205
Merge branch 'main' into mcp-apps
ochafik Dec 12, 2025
a312e52
feat: use ext-apps branch with setter-based MCP forwarding handlers
ochafik Dec 12, 2025
cae7999
Merge upstream/main into mcp-apps
ochafik Dec 12, 2025
3cf3c37
feat(client): expose MCP request handlers and AppBridge ref
ochafik Dec 12, 2025
e6dbe15
refactor(client): require AppBridge in AppFrame, cleaner AppRenderer API
ochafik Dec 12, 2025
fc16af4
refactor(client): require AppBridge in AppFrame, cleaner AppRenderer API
ochafik Dec 12, 2025
196401e
Merge origin/mcp-apps and use hostContext prop instead of setHostCont…
ochafik Dec 12, 2025
681bb43
chore: prettier formatting + re-export McpUiHostContext type
ochafik Dec 12, 2025
a84faee
refactor(client): cleaner API with props instead of ref methods
ochafik Dec 12, 2025
fc32fc6
refactor(client): use camelCase for all callback props
ochafik Dec 12, 2025
f750c98
test(client): add comprehensive tests for AppRenderer
ochafik Dec 12, 2025
d2d350c
chore: update ext-apps to latest, fix test types
ochafik Dec 13, 2025
38a7fd0
chore: add ESLint flat config for v9 compatibility
ochafik Dec 13, 2025
93f8759
chore: switch ext-apps dependency to main branch
ochafik Dec 15, 2025
b2673b9
chore(client): regenerate iframe-bundle with updated dependencies
ochafik Dec 16, 2025
fece80e
feat(client): make Client optional with onReadResource alternative
ochafik Dec 16, 2025
23f28e9
chore: update ext-apps to ^0.2.0 and MCP SDK to ^1.24.0
ochafik Dec 16, 2025
28f61b7
docs: add AppRenderer/AppFrame docs, fix sandbox promise rejection
ochafik Dec 16, 2025
8ff3186
Merge origin/main into mcp-apps to update PR #147
idosal Dec 18, 2025
0d64f86
fix build
idosal Dec 18, 2025
d49b412
remove mcp-ui/shared dependency
idosal Dec 20, 2025
9bfcea7
remove deprecated AppRenderer API
idosal Dec 20, 2025
73b7696
fix lifecycle
idosal Dec 20, 2025
9be1bf9
Merge origin/main into mcp-apps - resolve UIResourceRendererWC.tsx co…
idosal Dec 20, 2025
9244180
use experimental ext-apps for types
idosal Dec 22, 2025
c042d9c
pnpm lock
idosal Dec 22, 2025
f960673
Update sdks/typescript/client/src/components/AppFrame.tsx
ochafik Jan 7, 2026
4b67191
fix: remove onLoggingMessage from AppFrame
ochafik Jan 7, 2026
c3d0806
Update to latest sdk changes: registerApp*, _meta.ui, getToolUiResour…
ochafik Jan 7, 2026
387879b
chore: upgrade @modelcontextprotocol/ext-apps to 0.3.1
ochafik Jan 9, 2026
3fc3435
refactor: use SANDBOX_PROXY_READY_METHOD from ext-apps 0.3.1
ochafik Jan 9, 2026
d4c623a
feat(client): add UI extension capabilities for client capability neg…
ochafik Jan 10, 2026
558d915
fix(server): wrap case block with braces to fix no-case-declarations …
ochafik Jan 10, 2026
d256c63
feat(client): add CSP query parameter support for HTTP header-based CSP
ochafik Jan 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
import reactPlugin from 'eslint-plugin-react';
import prettier from 'eslint-config-prettier';

export default [
{
ignores: [
'node_modules/**',
'dist/**',
'coverage/**',
'**/*.log',
'**/*.js',
'**/*.mjs',
'**/*.cjs',
'sdks/typescript/client/src/remote-dom/iframe-bundle.ts',
'examples/**',
],
},
{
files: ['**/*.ts', '**/*.tsx'],
plugins: {
'@typescript-eslint': typescriptEslint,
react: reactPlugin,
},
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
},
settings: {
react: {
version: 'detect',
},
},
rules: {
...typescriptEslint.configs.recommended.rules,
...reactPlugin.configs.recommended.rules,
...reactPlugin.configs['jsx-runtime'].rules,
'react/prop-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-empty-object-type': 'off',
},
},
prettier,
];
2 changes: 1 addition & 1 deletion examples/mcp-apps-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"dependencies": {
"@mcp-ui/server": "workspace:*",
"@modelcontextprotocol/ext-apps": "https://github.com/modelcontextprotocol/ext-apps/archive/192d29f2ba122d0ad22e4456b9fd6b1e0fa7110f.tar.gz",
"@modelcontextprotocol/ext-apps": "github:modelcontextprotocol/ext-apps#main",

Copilot AI Dec 16, 2025

Copy link

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. This is not a stable versioning strategy and can lead to unpredictable behavior. 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#v1.0.0",

Copilot uses AI. Check for mistakes.
"@modelcontextprotocol/sdk": "^1.22.0",
"cors": "^2.8.5",
"express": "^4.18.2",
Expand Down
5,010 changes: 2,298 additions & 2,712 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions sdks/typescript/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@
"dist"
],
"dependencies": {
"@modelcontextprotocol/sdk": "^1.22.0",
"@modelcontextprotocol/ext-apps": "github:modelcontextprotocol/ext-apps#main",

Copilot AI Dec 16, 2025

Copy link

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.
"@modelcontextprotocol/sdk": "^1.23.0",
"@mcp-ui/shared": "workspace:*",

Copy link
Copy Markdown
Contributor

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Removed. Thanks @infoxicator!

"@quilted/threads": "^3.1.3",
"@r2wc/react-to-web-component": "^2.0.4",
"@remote-dom/core": "^1.8.0",
"@remote-dom/react": "^1.2.2"
"@remote-dom/react": "^1.2.2",
"zod": "^3.23.8"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.0.0",
Expand Down
253 changes: 253 additions & 0 deletions sdks/typescript/client/src/components/AppFrame.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import { useEffect, useRef, useState } from 'react';

import type {
CallToolResult,
LoggingMessageNotification,
Implementation,
} from '@modelcontextprotocol/sdk/types.js';

import {
AppBridge,
PostMessageTransport,
type McpUiSizeChangedNotification,
type McpUiResourceCsp,
type McpUiAppCapabilities,
} from '@modelcontextprotocol/ext-apps/app-bridge';

import { setupSandboxProxyIframe } from '../utils/app-host-utils';

/**
* Information about the guest app, available after initialization.
*/
export interface AppInfo {
/** Guest app's name and version */
appVersion?: Implementation;
/** Guest app's declared capabilities */
appCapabilities?: McpUiAppCapabilities;
}

/**
* Sandbox configuration for the iframe.
*/
export interface SandboxConfig {
/** URL to the sandbox proxy HTML */
url: URL;
/** Override iframe sandbox attribute (default: "allow-scripts allow-same-origin allow-forms") */
permissions?: string;
/** CSP metadata to forward to the sandbox proxy */
csp?: McpUiResourceCsp;
}

/**
* Props for the AppFrame component.
*/
export interface AppFrameProps {
/** Pre-fetched HTML content to render in the sandbox */
html: string;

/** Sandbox configuration */
sandbox: SandboxConfig;

/** Pre-configured AppBridge for MCP communication (required) */
appBridge: AppBridge;

/** Callback when guest reports size change */
onSizeChanged?: (params: McpUiSizeChangedNotification['params']) => void;

/** Callback when guest sends a logging message */
onLoggingMessage?: (params: LoggingMessageNotification['params']) => void;

/** Callback when app initialization completes, with app info */
onInitialized?: (appInfo: AppInfo) => void;

/** Tool input arguments to send when app initializes */
toolInput?: Record<string, unknown>;

/** Tool result to send when app initializes */
toolResult?: CallToolResult;

/** Callback when an error occurs */
onError?: (error: Error) => void;
}

/**
* Low-level component that renders pre-fetched HTML in a sandboxed iframe.
*
* This component requires a pre-configured AppBridge for MCP communication.
* For automatic AppBridge creation and resource fetching, use the higher-level
* AppRenderer component instead.
*
* @example With pre-configured AppBridge
* ```tsx
* const appBridge = new AppBridge(client, hostInfo, capabilities);
* // ... configure appBridge handlers ...
*
* <AppFrame
* html={htmlContent}
* sandbox={{ url: sandboxUrl }}
* appBridge={appBridge}
* toolInput={args}
* toolResult={result}
* onSizeChanged={({ width, height }) => console.log('Size:', width, height)}
* />
* ```
*/
export const AppFrame = (props: AppFrameProps) => {
const {
html,
sandbox,
appBridge,
onSizeChanged,
onLoggingMessage,
onInitialized,
toolInput,
toolResult,
onError,
} = props;

const [iframeReady, setIframeReady] = useState(false);
const [bridgeConnected, setBridgeConnected] = useState(false);
const [error, setError] = useState<Error | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const iframeRef = useRef<HTMLIFrameElement | null>(null);

// Refs for callbacks to avoid effect re-runs
const onSizeChangedRef = useRef(onSizeChanged);
const onLoggingMessageRef = useRef(onLoggingMessage);
const onInitializedRef = useRef(onInitialized);
const onErrorRef = useRef(onError);

useEffect(() => {
onSizeChangedRef.current = onSizeChanged;
onLoggingMessageRef.current = onLoggingMessage;
onInitializedRef.current = onInitialized;
onErrorRef.current = onError;
});

// Effect 1: Set up sandbox iframe and connect AppBridge
useEffect(() => {
let mounted = true;

const setup = async () => {
try {
const { iframe, onReady } = await setupSandboxProxyIframe(sandbox.url);

if (!mounted) return;

iframeRef.current = iframe;
if (containerRef.current) {
containerRef.current.appendChild(iframe);
}

await onReady;

if (!mounted) return;

// Register size change handler
appBridge.onsizechange = async (params) => {
onSizeChangedRef.current?.(params);
// Also update iframe size
if (iframeRef.current) {
if (params.width !== undefined) {
iframeRef.current.style.width = `${params.width}px`;
}
if (params.height !== undefined) {
iframeRef.current.style.height = `${params.height}px`;
}
}
};

// Hook into initialization
appBridge.oninitialized = () => {
if (!mounted) return;
console.log('[AppFrame] App initialized');

Copilot AI Dec 16, 2025

Copy link

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.
setIframeReady(true);
onInitializedRef.current?.({
appVersion: appBridge.getAppVersion(),
appCapabilities: appBridge.getAppCapabilities(),
});
};

// Register logging handler
appBridge.onloggingmessage = (params) => {
onLoggingMessageRef.current?.(params);
};
Comment thread
ochafik marked this conversation as resolved.
Outdated

// Connect the bridge
await appBridge.connect(
new PostMessageTransport(iframe.contentWindow!, iframe.contentWindow!),
);

if (!mounted) return;

setBridgeConnected(true);
} catch (err) {
console.error('[AppFrame] Error:', err);
if (!mounted) return;
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
onErrorRef.current?.(error);
}
};

setup();

return () => {
mounted = false;
if (iframeRef.current && containerRef.current?.contains(iframeRef.current)) {
containerRef.current.removeChild(iframeRef.current);
}

Copy link
Copy Markdown
Contributor

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?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @infoxicator , please review 73b7696 (cc @ochafik)

};
}, [sandbox.url, appBridge]);

// Effect 2: Send HTML to sandbox when bridge is connected
useEffect(() => {
if (!bridgeConnected || !html) return;

const sendHtml = async () => {
try {
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);
Comment on lines +260 to +288

Copilot AI Dec 16, 2025

Copy link

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.
appBridge.sendToolResult(toolResult);
}
}, [appBridge, bridgeConnected, iframeReady, toolResult]);

return (
<div
ref={containerRef}
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
{error && <div style={{ color: 'red', padding: '1rem' }}>Error: {error.message}</div>}
</div>
);
};
Loading
Loading