-
Notifications
You must be signed in to change notification settings - Fork 68
fix: proper CSP handling in basic-host sandboxing (HTTP headers, worker-src, document.write) #234
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
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(), |
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.
@claude do I understand correctly that we have a risk of csp injection here?
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.
Pushed a fix, ptal @antonpk1
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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