| Platform | Network | External Scripts | Widget Modes | Response Format |
|---|
| OpenAI | Open | CDN allowed | inline, fullscreen, pip | _meta['ui/html'] |
| ext-apps | Open | CDN allowed | inline, fullscreen, pip | _meta['ui/html'] |
| Cursor | Open | CDN allowed | inline | _meta['ui/html'] |
| Claude | Blocked | Cloudflare CDN only | Artifacts | Dual-payload |
| Continue | Limited | Inline only | inline | _meta['ui/html'] |
| Cody | Limited | Inline only | inline | _meta['ui/html'] |
| Gemini | Limited | Inline preferred | Basic | JSON only |
| generic-mcp | Varies | CDN/Inline | inline, static | _meta['ui/html'] |
FrontMCP automatically detects the platform:
import { getPlatform, OPENAI_PLATFORM, CLAUDE_PLATFORM } from '@frontmcp/ui';
// Auto-detect from user agent or context
const platform = getPlatform();
// Check capabilities
if (platform.network === 'open') {
// Can fetch external resources
}
if (platform.scripts === 'inline') {
// Must inline all scripts
}
OpenAI
OpenAI’s Apps SDK provides full widget capabilities:
Features
- Full network access
- External CDN scripts (Tailwind, HTMX)
- Multiple display modes (inline, fullscreen, pip)
- Theme detection
- Tool invocation from widgets
- Widget state persistence
Configuration
import { OPENAI_PLATFORM } from '@frontmcp/ui';
// Platform preset
console.log(OPENAI_PLATFORM);
// {
// id: 'openai',
// network: 'open',
// scripts: 'external',
// capabilities: {
// callTool: true,
// sendMessage: true,
// openExternal: true,
// requestDisplayMode: true,
// widgetState: true,
// },
// }
Runtime API
OpenAI exposes window.openai:
// Properties
window.openai.theme // 'light' | 'dark'
window.openai.displayMode // 'inline' | 'fullscreen' | 'pip'
window.openai.toolInput // Tool input arguments
window.openai.toolOutput // Tool output/result
window.openai.userAgent // Device info
// Methods
window.openai.callTool(name, args) // Call MCP tool
window.openai.sendFollowUpMessage({ prompt }) // Send chat message
window.openai.openExternal({ href }) // Open link
window.openai.requestDisplayMode({ mode }) // Change display mode
window.openai.setWidgetState(state) // Persist state
Claude
Claude uses Artifacts with restricted capabilities and a special dual-payload response format.
Constraints
- Network blocked - Cannot fetch most external resources
- Cloudflare CDN allowed - Resources from
cdnjs.cloudflare.com are trusted
- Limited interactivity - No tool invocation from widgets
When servingMode: 'auto' detects a Claude client, FrontMCP returns a special two-block response:
{
"content": [
{ "type": "text", "text": "{\"temperature\":72,\"unit\":\"F\"}" },
{ "type": "text", "text": "Here is the visual result:\n\n```html\n<!DOCTYPE html>...\n```" }
]
}
- Block 0: Pure JSON data for programmatic parsing
- Block 1: Markdown-wrapped HTML that Claude displays as an Artifact
Claude automatically detects the html code fence and offers to render it as an interactive Artifact.
Customizing the HTML Prefix
Control the text shown before the HTML block:
@Tool({
name: 'get_weather',
ui: {
template: WeatherWidget,
htmlResponsePrefix: 'Here is the weather dashboard',
},
})
// Output: "Here is the weather dashboard:\n\n```html\n..."
Default: 'Here is the visual result'
Cloudflare CDN for Tailwind
Claude trusts resources from cdnjs.cloudflare.com. FrontMCP uses pre-built Tailwind CSS from Cloudflare:
// Pre-built Tailwind CSS (loaded automatically with resourceMode: 'cdn')
const TAILWIND_CDN = 'https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css';
Other CDNs like esm.sh, unpkg.com, and jsdelivr.net are blocked by Claude’s CSP. Only use cdnjs.cloudflare.com for maximum compatibility.
resourceMode Configuration
Control how external resources are loaded:
ui: {
template: MyWidget,
resourceMode: 'cdn', // Load from Cloudflare CDN (default)
// or
resourceMode: 'inline', // Embed all scripts in HTML (fully offline)
}
Configuration
import { CLAUDE_PLATFORM } from '@frontmcp/ui';
// Platform preset
console.log(CLAUDE_PLATFORM);
// {
// id: 'claude',
// network: 'blocked',
// scripts: 'inline',
// capabilities: {
// callTool: false,
// sendMessage: false,
// openExternal: true,
// requestDisplayMode: false,
// widgetState: false,
// },
// }
Handling Fully Blocked Networks
For environments without any network access, inline all scripts:
import { baseLayout, fetchAndCacheScriptsFromTheme } from '@frontmcp/ui';
// Pre-fetch and cache scripts at startup
await fetchAndCacheScriptsFromTheme(theme);
// Then build with inline scripts
const html = baseLayout({
content: '...',
theme,
platform: 'claude',
scripts: true, // Will inline cached scripts
});
Gemini
Gemini has limited widget support:
Configuration
import { GEMINI_PLATFORM } from '@frontmcp/ui';
// Platform preset
console.log(GEMINI_PLATFORM);
// {
// id: 'gemini',
// network: 'limited',
// scripts: 'inline',
// capabilities: {
// callTool: false,
// sendMessage: false,
// openExternal: true,
// requestDisplayMode: false,
// widgetState: false,
// },
// }
Best Practices
For Gemini, keep widgets simple:
- Use inline styles
- Avoid complex interactivity
- Focus on data display
Create templates that adapt to the platform:
import { getPlatform, canUseCdn, needsInlineScripts } from '@frontmcp/ui';
@Tool({
name: 'my_tool',
ui: {
template: (ctx) => {
const platform = getPlatform();
// Use CDN on capable platforms
const useExternalScripts = canUseCdn(platform);
// Build appropriate HTML
return buildWidgetHtml({
content: ctx.output,
inlineScripts: needsInlineScripts(platform),
useExternalScripts,
});
},
},
})
Fallback Modes
Handle missing capabilities gracefully:
import { supportsFullInteractivity, getFallbackMode } from '@frontmcp/ui';
const platform = getPlatform();
if (supportsFullInteractivity(platform)) {
// Full widget with tool calls
return interactiveWidget(ctx);
} else {
// Static display only
const fallback = getFallbackMode(platform);
return staticWidget(ctx, fallback);
}
Define custom platform configurations:
import { createPlatform } from '@frontmcp/ui';
const myPlatform = createPlatform({
id: 'my-platform',
network: 'open',
scripts: 'external',
capabilities: {
callTool: true,
sendMessage: true,
openExternal: true,
requestDisplayMode: false,
widgetState: true,
},
});
Content Security Policy
Configure CSP based on platform:
@Tool({
name: 'my_tool',
ui: {
template: ...,
csp: {
// OpenAI: Allow CDNs
connectDomains: ['api.example.com'],
resourceDomains: ['cdn.example.com', 'fonts.googleapis.com'],
},
},
})
For Claude (blocked network), CSP is largely irrelevant since external resources can’t be fetched anyway.
Test your widgets on each platform:
// Mock platform for testing
import { getPlatform } from '@frontmcp/ui';
// Override for testing
jest.mock('@frontmcp/ui', () => ({
...jest.requireActual('@frontmcp/ui'),
getPlatform: jest.fn().mockReturnValue({
id: 'claude',
network: 'blocked',
scripts: 'inline',
}),
}));
Best Practices
- Design for constraints - Start with Claude’s limitations, enhance for OpenAI
- Progressive enhancement - Add features based on platform capabilities
- Test on all platforms - Verify appearance and functionality
- Cache scripts - Pre-fetch for blocked-network platforms
- Provide fallbacks - Graceful degradation when features unavailable
- Document platform differences - Help users understand what to expect