fix: proper CSP handling in basic-host sandboxing (HTTP headers, worker-src, document.write)#234
fix: proper CSP handling in basic-host sandboxing (HTTP headers, worker-src, document.write)#234
Conversation
…Domains, permissions Security improvements: - CSP is now set via HTTP headers in serve.ts instead of meta tags (meta tag CSP can be tampered with by same-origin content) - CSP passed as query param to sandbox.html for header-based enforcement New CSP/permissions features (borrowed from PR #158): - frameDomains: control frame-src directive for nested iframes - baseUriDomains: control base-uri directive - permissions: camera, microphone, geolocation via iframe allow attribute WebGL fix: - Use document.write() instead of srcdoc for inner iframe content (srcdoc creates opaque origin that breaks WebGL canvas updates) - Add worker-src directive with blob: support (critical for WebGL apps like CesiumJS/Three.js that use workers for tile decoding, terrain processing, image processing) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@modelcontextprotocol/ext-apps
@modelcontextprotocol/server-basic-react
@modelcontextprotocol/server-basic-vanillajs
@modelcontextprotocol/server-budget-allocator
@modelcontextprotocol/server-cohort-heatmap
@modelcontextprotocol/server-customer-segmentation
@modelcontextprotocol/server-scenario-modeler
@modelcontextprotocol/server-system-monitor
@modelcontextprotocol/server-threejs
@modelcontextprotocol/server-wiki-explorer
commit: |
Keep this PR focused on CSP security fixes only. Permissions (camera, microphone, geolocation) will be handled in #158. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
55750c5 to
73b69e7
Compare
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
| // Images: same-origin + data/blob URIs + specified domains | ||
| `img-src 'self' data: blob: ${resourceDomains}`.trim(), | ||
| // Fonts: same-origin + data/blob URIs + specified domains | ||
| `font-src 'self' data: blob: ${resourceDomains}`.trim(), |
Validate CSP domain entries to reject characters that could: - Break out of CSP directives (semicolons, newlines) - Inject CSP keywords like 'unsafe-eval' (quotes) - Inject multiple sources in one entry (spaces) This prevents injection attacks where malicious domains could override the security policy.
| modifiedHtml = cspMetaTag + modifiedHtml; | ||
| } | ||
| // Use document.write instead of srcdoc for WebGL compatibility. | ||
| // srcdoc creates an opaque origin which prevents WebGL canvas updates |
There was a problem hiding this comment.
I'm confused by this bit. srcdoc iframes are not all opaque-origined. For example, navigate to https://example.com and run this in DevTools:
const iframe = document.createElement('iframe');
iframe.srcdoc = `<p id=log></p><script>log.textContent = self.origin</script>`;
document.body.append(iframe);By default, srcdoc iframes inherit their navigation initiator's origin (i.e., the origin of the document that set the srcdoc attribute), unless their sandbox attribute overrides this.
There was a problem hiding this comment.
Oops sorry good catch, will send update
Real reason: was to work around browser canvas tainting checks that seemed to treat about:srcdoc URLs as tainted, even when the iframe is same-origin with its parent. Will confirm and update the comment. in a followup
| severity: "high", | ||
| }, | ||
| { | ||
| target: "Element.prototype", |
There was a problem hiding this comment.
What dictates what's in this list? Is there documentation somewhere? APIs like setHTMLUnsafe() come to mind (maybe it's fine since it's already unsafe), but I was just curious how these were chosen.
There was a problem hiding this comment.
Oh this wasn't meant to be submitted, sorry :-(
Follow-up to PR #234. This file was accidentally included.
Update comment to accurately explain the canvas tainting issue rather than incorrectly claiming srcdoc creates an opaque origin. Follow-up to PR #234 per domfarolino's review feedback.
Follow-up to PR #234 per domfarolino's review feedback.
* UITemplatedToolCallRendererProps for MCP Apps * Upgrade MCP SDK to 1.22.0 (many (Embedded)Resource type fixes) * Update adapter.ts * Update UIResourceRendererWC.test.tsx * add missing client dep (vite-tsconfig-paths) * update to latest ext-apps example renderer * Sync with latest ext-apps, fix PR review comments - 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 <noreply@anthropic.com> * Add AppFrame component, refactor AppRenderer, use ext-apps v0.1.0 - 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 <noreply@anthropic.com> * feat: use ext-apps branch with setter-based MCP forwarding handlers - 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 * feat(client): expose MCP request handlers and AppBridge ref 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 * refactor(client): require AppBridge in AppFrame, cleaner AppRenderer API 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 * refactor(client): require AppBridge in AppFrame, cleaner AppRenderer API 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 * chore: prettier formatting + re-export McpUiHostContext type * refactor(client): cleaner API with props instead of ref methods 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 * refactor(client): use camelCase for all callback props 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) * test(client): add comprehensive tests for AppRenderer 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) * chore: update ext-apps to latest, fix test types - 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 * chore: add ESLint flat config for v9 compatibility - 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) * chore: switch ext-apps dependency to main branch The ochafik/app-bridge-setters branch has been merged to main. * chore(client): regenerate iframe-bundle with updated dependencies - @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 <noreply@anthropic.com> * feat(client): make Client optional with onReadResource alternative 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 <noreply@anthropic.com> * chore: update ext-apps to ^0.2.0 and MCP SDK to ^1.24.0 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 <noreply@anthropic.com> * docs: add AppRenderer/AppFrame docs, fix sandbox promise rejection - 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 <noreply@anthropic.com> * fix build * remove mcp-ui/shared dependency It wasn't used and caused install to fail * remove deprecated AppRenderer API * fix lifecycle * use experimental ext-apps for types * pnpm lock * Update sdks/typescript/client/src/components/AppFrame.tsx Co-authored-by: Ruben Casas <ruben@infoxication.net> * fix: remove onLoggingMessage from AppFrame 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. * Update to latest sdk changes: registerApp*, _meta.ui, getToolUiResourceUri * chore: upgrade @modelcontextprotocol/ext-apps to 0.3.1 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 * refactor: use SANDBOX_PROXY_READY_METHOD from ext-apps 0.3.1 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 * feat(client): add UI extension capabilities for client capability negotiation 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> * fix(server): wrap case block with braces to fix no-case-declarations 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> * feat(client): add CSP query parameter support for HTTP header-based CSP 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 --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Ido Salomon <idosalomon@gmail.com> Co-authored-by: Ruben Casas <ruben@infoxication.net>
This PR tightens CSP handling in basic-host (which was setting very loose permissions so far).
Changes
frameDomains,baseUriDomains(borrowed from feat: enhance sandbox capability negotiation #158, cc/ @idosal @domfarolino) - Extend CSP configuration optionsworker-srcdirective - Needed for WebGL apps (CesiumJS, Three.js) that use Web Workers for tile decoding, terrain processing, etc.document.write()instead ofsrcdoc- Fixes WebGL rendering issues (srcdoc creates opaque origin that breaks canvas updates)(last two points fix the upcoming Map App example #235 )
Implementation
Note: basic-host is still not to be considered as a hardened production host.
🤖 Generated with Claude Code