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';
Copy link
Contributor

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
Contributor

@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.

Copy link
Contributor

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)
"@modelcontextprotocol/sdk": "^1.22.0",
"@modelcontextprotocol/ext-apps": "^0.2.0",
"@modelcontextprotocol/sdk": "^1.24.0",
"@mcp-ui/shared": "workspace:*",
Copy link
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
Collaborator

Choose a reason for hiding this comment

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

Removed. Thanks @infoxicator!

Comment on lines 197 to 199
if (iframeRef.current && containerRef.current?.contains(iframeRef.current)) {
containerRef.current.removeChild(iframeRef.current);
}
Copy link
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
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 configuration */
sandbox: SandboxConfig;

/** @deprecated Use `sandbox.url` instead */
Copy link
Contributor

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?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Removed. Thanks @infoxicator !

- 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

It wasn't used and caused install to fail
…nflict

Keep the connectedMoveCallback implementation from main branch which
adds atomic move support for DOM element repositioning, preventing
iframe reloads when elements are moved via moveBefore().
@idosal
Copy link
Collaborator

idosal commented Dec 23, 2025

@ochafik - updated the proxy ready method name to use the new exported const from ext-apps. It uses the PR package to unblock alpha testers, but I'll switch it back to the official version after the ext-apps PR is merged.

AppFrame is a low-level component that takes a pre-configured appBridge.
The caller owns the bridge and should configure handlers directly.
This prevents AppFrame from overwriting handlers set by AppRenderer.

onLoggingMessage remains available in AppRenderer.
@ochafik
Copy link
Contributor Author

ochafik commented Jan 7, 2026

@ochafik - updated the proxy ready method name to use the new exported const from ext-apps. It uses the PR package to unblock alpha testers, but I'll switch it back to the official version after the ext-apps PR is merged.

@idosal Not sure what happened to that const, i've defined it locally (in a typesafe way referring to the message) along w/ a few tweaks / using new utils.

Breaking changes addressed:
- Renamed viewport.maxHeight to containerDimensions.maxHeight
- Updated type handling for the new union type structure
- Fixed vitest config to exclude .pnpm-store temp files

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 0
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Now that @modelcontextprotocol/ext-apps exports method constants,
import SANDBOX_PROXY_READY_METHOD directly instead of defining it locally.

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 0
Claude-Permission-Prompts: 0
Claude-Escapes: 0
…otiation

Add typed helpers for declaring UI extension support when connecting to MCP
servers, following the SEP-1724 extensions field pattern.

New exports from @mcp-ui/client:
- ClientCapabilitiesWithExtensions: Extended type with extensions field
- UI_EXTENSION_NAME: Extension identifier 'io.modelcontextprotocol/ui'
- UI_EXTENSION_CONFIG: Config with mimeTypes array
- UI_EXTENSION_CAPABILITIES: Ready-to-use capabilities object

This enables consumers to declare UI extension support when creating MCP clients:

  const client = new Client(
    { name: 'my-app', version: '1.0.0' },
    { capabilities: { extensions: UI_EXTENSION_CAPABILITIES } }
  );

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 1
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# Plan: Add Client Capabilities for UI Extension Support

## Context

The mcp-ui client SDK helps consumers render MCP UI resources, but it doesn't currently help them **declare UI extension support** when connecting to MCP servers. This is important because:

1. Servers may filter/adjust responses based on client capabilities
2. SEP-1724 proposes an `extensions` field for capability negotiation
3. Consumers shouldn't have to figure out the right structure themselves

## Design Analysis

### Current State
- MCP SDK's `ClientCapabilities` has an `experimental` field (open record)
- SEP-1724 proposes a new `extensions` field (not yet adopted)
- ext-apps exports `RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"`
- The mcp-ui client SDK takes an MCP `Client` as input but doesn't help configure it

### Design Decision

**Approach**: Export typed helpers from mcp-ui client SDK using the `extensions` field pattern proposed in SEP-1724.

```typescript
// Usage by consumer:
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import {
  type ClientCapabilitiesWithExtensions,
  UI_EXTENSION_CAPABILITIES,
} from '@mcp-ui/client';

const capabilities: ClientCapabilitiesWithExtensions = {
  // Standard MCP capabilities
  roots: { listChanged: true },
  // UI extension (SEP-1724 pattern)
  extensions: UI_EXTENSION_CAPABILITIES,
};

const client = new Client(
  { name: 'my-app', version: '1.0.0' },
  { capabilities }
);
```

### Why This Approach

1. **Forward-compatible**: Follows SEP-1724 pattern, ready for adoption
2. **Type-safe**: Custom type extension makes intent explicit
3. **Clear semantics**: `extensions` field is specifically for extensions, not `experimental`
4. **Documented pattern**: Links to SEP-1724 for context

## Implementation Plan

### 1. Add capabilities module to client SDK

**File**: `sdks/typescript/client/src/capabilities.ts` (new file)

```typescript
import type { ClientCapabilities } from '@modelcontextprotocol/sdk/types.js';
import { RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/app-bridge';

// Custom type ahead of the Extensions SEP making it to MCP
// modelcontextprotocol/modelcontextprotocol#1724
export interface ClientCapabilitiesWithExtensions extends ClientCapabilities {
  extensions?: {
    [extensionName: string]: unknown;
  };
}

/**
 * Extension identifier for MCP UI support.
 * Follows the pattern from SEP-1724: {vendor-prefix}/{extension-name}
 */
export const UI_EXTENSION_NAME = 'io.modelcontextprotocol/ui' as const;

/**
 * UI extension capability configuration.
 * Declares support for rendering UI resources.
 */
export const UI_EXTENSION_CONFIG = {
  mimeTypes: [RESOURCE_MIME_TYPE],
} as const;

/**
 * UI extension capabilities object to use in the `extensions` field.
 *
 * @example
 * ```typescript
 * import { Client } from '@modelcontextprotocol/sdk/client/index.js';
 * import {
 *   type ClientCapabilitiesWithExtensions,
 *   UI_EXTENSION_CAPABILITIES,
 * } from '@mcp-ui/client';
 *
 * const capabilities: ClientCapabilitiesWithExtensions = {
 *   extensions: UI_EXTENSION_CAPABILITIES,
 * };
 *
 * const client = new Client(
 *   { name: 'my-app', version: '1.0.0' },
 *   { capabilities }
 * );
 * ```
 */
export const UI_EXTENSION_CAPABILITIES = {
  [UI_EXTENSION_NAME]: UI_EXTENSION_CONFIG,
} as const;
```

### 2. Export from index.ts

**File**: `sdks/typescript/client/src/index.ts`

Add exports:
```typescript
// Client capabilities for UI extension support (SEP-1724)
export {
  type ClientCapabilitiesWithExtensions,
  UI_EXTENSION_NAME,
  UI_EXTENSION_CONFIG,
  UI_EXTENSION_CAPABILITIES,
} from './capabilities';
```

### 3. Add unit tests

**File**: `sdks/typescript/client/src/__tests__/capabilities.test.ts`

```typescript
import { describe, it, expect } from 'vitest';
import {
  type ClientCapabilitiesWithExtensions,
  UI_EXTENSION_NAME,
  UI_EXTENSION_CONFIG,
  UI_EXTENSION_CAPABILITIES,
} from '../capabilities';
import { RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/app-bridge';

describe('UI Extension Capabilities', () => {
  it('should have correct extension name', () => {
    expect(UI_EXTENSION_NAME).toBe('io.modelcontextprotocol/ui');
  });

  it('should include RESOURCE_MIME_TYPE in mimeTypes', () => {
    expect(UI_EXTENSION_CONFIG.mimeTypes).toContain(RESOURCE_MIME_TYPE);
  });

  it('should structure capabilities with extension name as key', () => {
    expect(UI_EXTENSION_CAPABILITIES[UI_EXTENSION_NAME]).toEqual(
      UI_EXTENSION_CONFIG
    );
  });

  it('should work with ClientCapabilitiesWithExtensions type', () => {
    const capabilities: ClientCapabilitiesWithExtensions = {
      roots: { listChanged: true },
      extensions: UI_EXTENSION_CAPABILITIES,
    };

    expect(capabilities.roots).toEqual({ listChanged: true });
    expect(capabilities.extensions?.[UI_EXTENSION_NAME]).toEqual(UI_EXTENSION_CONFIG);
  });
});
```

### 4. Update documentation

**File**: `docs/src/guide/mcp-apps.md`

Add section on client configuration:
```markdown
## Declaring UI Extension Support

When creating your MCP client, declare UI extension support using the provided type and capabilities:

\`\`\`typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import {
  type ClientCapabilitiesWithExtensions,
  UI_EXTENSION_CAPABILITIES,
} from '@mcp-ui/client';

const capabilities: ClientCapabilitiesWithExtensions = {
  // Standard capabilities
  roots: { listChanged: true },
  // UI extension support (SEP-1724 pattern)
  extensions: UI_EXTENSION_CAPABILITIES,
};

const client = new Client(
  { name: 'my-app', version: '1.0.0' },
  { capabilities }
);
\`\`\`

This tells MCP servers that your client can render UI resources with MIME type \`text/html;profile=mcp-app\`.

> **Note:** This uses the \`extensions\` field pattern from [SEP-1724](modelcontextprotocol/modelcontextprotocol#1724), which is not yet part of the official MCP protocol.
```

## Files to Modify

1. `sdks/typescript/client/src/capabilities.ts` - **NEW** - capability constants
2. `sdks/typescript/client/src/index.ts` - add export
3. `sdks/typescript/client/src/__tests__/capabilities.test.ts` - **NEW** - unit tests
4. `docs/src/guide/mcp-apps.md` - add documentation section

## Verification

1. Run tests: `cd sdks/typescript/client && pnpm test`
2. Build SDK: `pnpm build`
3. Verify exports: Check that `UI_EXTENSION_CAPABILITIES` is exported correctly
4. Integration check: The capabilities object should be spreadable into MCP Client options

## Future Considerations

When SEP-1724 is adopted into MCP SDK:
1. Remove `ClientCapabilitiesWithExtensions` type extension
2. Update imports to use the official SDK type
3. No changes needed to `UI_EXTENSION_CAPABILITIES` structure
</claude-plan>
…lint error

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 0
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# Plan: Add Client Capabilities for UI Extension Support

## Context

The mcp-ui client SDK helps consumers render MCP UI resources, but it doesn't currently help them **declare UI extension support** when connecting to MCP servers. This is important because:

1. Servers may filter/adjust responses based on client capabilities
2. SEP-1724 proposes an `extensions` field for capability negotiation
3. Consumers shouldn't have to figure out the right structure themselves

## Design Analysis

### Current State
- MCP SDK's `ClientCapabilities` has an `experimental` field (open record)
- SEP-1724 proposes a new `extensions` field (not yet adopted)
- ext-apps exports `RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"`
- The mcp-ui client SDK takes an MCP `Client` as input but doesn't help configure it

### Design Decision

**Approach**: Export typed helpers from mcp-ui client SDK using the `extensions` field pattern proposed in SEP-1724.

```typescript
// Usage by consumer:
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import {
  type ClientCapabilitiesWithExtensions,
  UI_EXTENSION_CAPABILITIES,
} from '@mcp-ui/client';

const capabilities: ClientCapabilitiesWithExtensions = {
  // Standard MCP capabilities
  roots: { listChanged: true },
  // UI extension (SEP-1724 pattern)
  extensions: UI_EXTENSION_CAPABILITIES,
};

const client = new Client(
  { name: 'my-app', version: '1.0.0' },
  { capabilities }
);
```

### Why This Approach

1. **Forward-compatible**: Follows SEP-1724 pattern, ready for adoption
2. **Type-safe**: Custom type extension makes intent explicit
3. **Clear semantics**: `extensions` field is specifically for extensions, not `experimental`
4. **Documented pattern**: Links to SEP-1724 for context

## Implementation Plan

### 1. Add capabilities module to client SDK

**File**: `sdks/typescript/client/src/capabilities.ts` (new file)

```typescript
import type { ClientCapabilities } from '@modelcontextprotocol/sdk/types.js';
import { RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/app-bridge';

// Custom type ahead of the Extensions SEP making it to MCP
// modelcontextprotocol/modelcontextprotocol#1724
export interface ClientCapabilitiesWithExtensions extends ClientCapabilities {
  extensions?: {
    [extensionName: string]: unknown;
  };
}

/**
 * Extension identifier for MCP UI support.
 * Follows the pattern from SEP-1724: {vendor-prefix}/{extension-name}
 */
export const UI_EXTENSION_NAME = 'io.modelcontextprotocol/ui' as const;

/**
 * UI extension capability configuration.
 * Declares support for rendering UI resources.
 */
export const UI_EXTENSION_CONFIG = {
  mimeTypes: [RESOURCE_MIME_TYPE],
} as const;

/**
 * UI extension capabilities object to use in the `extensions` field.
 *
 * @example
 * ```typescript
 * import { Client } from '@modelcontextprotocol/sdk/client/index.js';
 * import {
 *   type ClientCapabilitiesWithExtensions,
 *   UI_EXTENSION_CAPABILITIES,
 * } from '@mcp-ui/client';
 *
 * const capabilities: ClientCapabilitiesWithExtensions = {
 *   extensions: UI_EXTENSION_CAPABILITIES,
 * };
 *
 * const client = new Client(
 *   { name: 'my-app', version: '1.0.0' },
 *   { capabilities }
 * );
 * ```
 */
export const UI_EXTENSION_CAPABILITIES = {
  [UI_EXTENSION_NAME]: UI_EXTENSION_CONFIG,
} as const;
```

### 2. Export from index.ts

**File**: `sdks/typescript/client/src/index.ts`

Add exports:
```typescript
// Client capabilities for UI extension support (SEP-1724)
export {
  type ClientCapabilitiesWithExtensions,
  UI_EXTENSION_NAME,
  UI_EXTENSION_CONFIG,
  UI_EXTENSION_CAPABILITIES,
} from './capabilities';
```

### 3. Add unit tests

**File**: `sdks/typescript/client/src/__tests__/capabilities.test.ts`

```typescript
import { describe, it, expect } from 'vitest';
import {
  type ClientCapabilitiesWithExtensions,
  UI_EXTENSION_NAME,
  UI_EXTENSION_CONFIG,
  UI_EXTENSION_CAPABILITIES,
} from '../capabilities';
import { RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/app-bridge';

describe('UI Extension Capabilities', () => {
  it('should have correct extension name', () => {
    expect(UI_EXTENSION_NAME).toBe('io.modelcontextprotocol/ui');
  });

  it('should include RESOURCE_MIME_TYPE in mimeTypes', () => {
    expect(UI_EXTENSION_CONFIG.mimeTypes).toContain(RESOURCE_MIME_TYPE);
  });

  it('should structure capabilities with extension name as key', () => {
    expect(UI_EXTENSION_CAPABILITIES[UI_EXTENSION_NAME]).toEqual(
      UI_EXTENSION_CONFIG
    );
  });

  it('should work with ClientCapabilitiesWithExtensions type', () => {
    const capabilities: ClientCapabilitiesWithExtensions = {
      roots: { listChanged: true },
      extensions: UI_EXTENSION_CAPABILITIES,
    };

    expect(capabilities.roots).toEqual({ listChanged: true });
    expect(capabilities.extensions?.[UI_EXTENSION_NAME]).toEqual(UI_EXTENSION_CONFIG);
  });
});
```

### 4. Update documentation

**File**: `docs/src/guide/mcp-apps.md`

Add section on client configuration:
```markdown
## Declaring UI Extension Support

When creating your MCP client, declare UI extension support using the provided type and capabilities:

\`\`\`typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import {
  type ClientCapabilitiesWithExtensions,
  UI_EXTENSION_CAPABILITIES,
} from '@mcp-ui/client';

const capabilities: ClientCapabilitiesWithExtensions = {
  // Standard capabilities
  roots: { listChanged: true },
  // UI extension support (SEP-1724 pattern)
  extensions: UI_EXTENSION_CAPABILITIES,
};

const client = new Client(
  { name: 'my-app', version: '1.0.0' },
  { capabilities }
);
\`\`\`

This tells MCP servers that your client can render UI resources with MIME type \`text/html;profile=mcp-app\`.

> **Note:** This uses the \`extensions\` field pattern from [SEP-1724](modelcontextprotocol/modelcontextprotocol#1724), which is not yet part of the official MCP protocol.
```

## Files to Modify

1. `sdks/typescript/client/src/capabilities.ts` - **NEW** - capability constants
2. `sdks/typescript/client/src/index.ts` - add export
3. `sdks/typescript/client/src/__tests__/capabilities.test.ts` - **NEW** - unit tests
4. `docs/src/guide/mcp-apps.md` - add documentation section

## Verification

1. Run tests: `cd sdks/typescript/client && pnpm test`
2. Build SDK: `pnpm build`
3. Verify exports: Check that `UI_EXTENSION_CAPABILITIES` is exported correctly
4. Integration check: The capabilities object should be spreadable into MCP Client options

## Future Considerations

When SEP-1724 is adopted into MCP SDK:
1. Remove `ClientCapabilitiesWithExtensions` type extension
2. Update imports to use the official SDK type
3. No changes needed to `UI_EXTENSION_CAPABILITIES` structure
</claude-plan>
Add support for passing CSP configuration via URL query parameter (?csp=<json>)
to the sandbox proxy. This enables proxy servers to set Content-Security-Policy
via HTTP headers (tamper-proof) rather than relying on meta tags or postMessage.

Changes:
- AppFrame.tsx: Build sandbox URL with CSP query param before loading iframe
- SandboxConfig.csp: Updated docs explaining query-param + postMessage fallback
- using-a-proxy.md: Added CSP Query Parameter section with server-side example
- Updated architecture diagram to show CSP flow through server

The CSP is still sent via postMessage as a fallback for proxies that don't
support the query parameter approach.

See: modelcontextprotocol/ext-apps#234
Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 2
Claude-Permission-Prompts: 0
Claude-Escapes: 0
@ochafik
Copy link
Contributor Author

ochafik commented Jan 11, 2026

Added some missing bits:

@ochafik
Copy link
Contributor Author

ochafik commented Jan 21, 2026

Gentle ping @idosal @liady , anything else needed here?

@idosal idosal merged commit fbc918d into MCP-UI-Org:main Jan 21, 2026
8 checks passed
@idosal
Copy link
Collaborator

idosal commented Jan 21, 2026

Gentle ping @idosal @liady , anything else needed here?

Was waiting for you 😄

Thanks a lot for contributing this critical capability @ochafik, you rock! Special thanks to @infoxicator and the community for helping push this through. ❤️

@infoxicator
Copy link
Contributor

Gentle ping @idosal @liady , anything else needed here?

Was waiting for you 😄

Thanks a lot for contributing this critical capability @ochafik, you rock! Special thanks to @infoxicator and the community for helping push this through. ❤️

You all rock! thanks for pushing this 👏

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