Skip to main content

Platform Capabilities

PlatformNetworkExternal ScriptsWidget ModesResponse Format
OpenAIOpenCDN allowedinline, fullscreen, pip_meta['ui/html']
ext-appsOpenCDN allowedinline, fullscreen, pip_meta['ui/html']
CursorOpenCDN allowedinline_meta['ui/html']
ClaudeBlockedCloudflare CDN onlyArtifactsDual-payload
ContinueLimitedInline onlyinline_meta['ui/html']
CodyLimitedInline onlyinline_meta['ui/html']
GeminiLimitedInline preferredBasicJSON only
generic-mcpVariesCDN/Inlineinline, static_meta['ui/html']

Platform Detection

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

Dual-Payload Response Format

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

Platform-Aware Templates

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);
}

Custom Platforms

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.

Testing Across Platforms

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

  1. Design for constraints - Start with Claude’s limitations, enhance for OpenAI
  2. Progressive enhancement - Add features based on platform capabilities
  3. Test on all platforms - Verify appearance and functionality
  4. Cache scripts - Pre-fetch for blocked-network platforms
  5. Provide fallbacks - Graceful degradation when features unavailable
  6. Document platform differences - Help users understand what to expect