Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
32f6572
feat: Add auto-transport persistence transformation for Redis configu…
frontegg-david Dec 23, 2025
bc2f84a
feat: Add auto-transport persistence transformation for Redis configu…
frontegg-david Dec 23, 2025
6fdcb38
feat: Enhance multi-platform bundling with theme support and metadata…
frontegg-david Dec 23, 2025
92a4076
Merge branch 'main' into fix-minify-ui-builder
frontegg-david Dec 23, 2025
a4ee898
feat: Implement build modes for static, dynamic, and hybrid HTML gene…
frontegg-david Dec 23, 2025
4fdd092
feat: Enhance dynamic data injection with platform-aware handling for…
frontegg-david Dec 23, 2025
ab74e02
feat: Add documentation for build modes and data injection strategies
frontegg-david Dec 23, 2025
e6bd540
refactor: Simplify adapter access and improve error handling in vario…
frontegg-david Dec 23, 2025
37ac1ab
refactor: Remove unused imports and simplify variable declarations ac…
frontegg-david Dec 23, 2025
317f77e
refactor: Remove unused imports and simplify variable declarations ac…
frontegg-david Dec 23, 2025
eb9ea9b
refactor: Clean up unused capabilities and improve platform handling …
frontegg-david Dec 23, 2025
b19ec32
feat: Implement package allowlist functionality in TypeFetcher
frontegg-david Dec 23, 2025
14e6ca4
refactor: Remove ngrok platform references and update platform handling
frontegg-david Dec 23, 2025
a3311b4
feat: Add browser-compatible UI components with esbuild integration
frontegg-david Dec 23, 2025
a557042
feat: Enable widget support for Claude Artifacts platform
frontegg-david Dec 23, 2025
e3cd703
feat: Add CSS to Tailwind theme conversion utility and update button …
frontegg-david Dec 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@ docs/docs.backup.json
.github/codex/

.npmrc
/libs/ui/.script.local
**/.script.local
/libs/uipack/tsconfig.lib.tsbuildinfo
1 change: 1 addition & 0 deletions docs/draft/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@
"icon": "bolt",
"pages": [
"docs/ui/advanced/platforms",
"docs/ui/advanced/build-modes",
"docs/ui/advanced/mcp-bridge",
"docs/ui/advanced/hydration",
"docs/ui/advanced/custom-renderers"
Expand Down
2 changes: 1 addition & 1 deletion docs/draft/docs/guides/ui-library.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const loginForm = form(

## Theme and platform detection

Use `createTheme` to adjust palettes, typography, and CDN endpoints. Then decide whether to inline scripts (Claude Artifacts) or load from the network (OpenAI, Gemini, ngrok).
Use `createTheme` to adjust palettes, typography, and CDN endpoints. Then decide whether to inline scripts (Claude Artifacts) or load from the network (OpenAI, Gemini).

```ts
import {
Expand Down
321 changes: 321 additions & 0 deletions docs/draft/docs/ui/advanced/build-modes.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
---
title: Build Modes
sidebarTitle: Build Modes
icon: hammer
description: Control how widget data is injected into HTML - static baking, dynamic subscription, or hybrid placeholder injection.
---

## Overview

FrontMCP supports three build modes that control how tool input/output data is injected into the rendered HTML:

| Mode | Description | Best For |
|------|-------------|----------|
| **static** | Data baked into HTML at build time | Default, simple widgets |
| **dynamic** | Platform-aware - subscribes to events (OpenAI) or uses placeholders (Claude) | Real-time updates, multi-platform |
| **hybrid** | Pre-built shell with placeholders, replace at runtime | Caching, high-performance |

## Static Mode (Default)

Data is serialized and embedded directly in the HTML at build time.

```typescript
const result = await bundler.bundleToStaticHTML({
source: componentCode,
toolName: 'get_weather',
output: { temperature: 72, unit: 'F' },
buildMode: 'static', // default
});
```

The generated HTML includes the data inline:

```html
<script>
window.__mcpToolOutput = {"temperature":72,"unit":"F"};
window.__frontmcp.setState({
toolName: "get_weather",
output: window.__mcpToolOutput,
loading: false,
error: null
});
</script>
```

**Use when:**
- Widget content doesn't change after initial render
- Simple tool responses
- No caching requirements

## Dynamic Mode

Dynamic mode is **platform-aware** - it behaves differently depending on the target platform:

### OpenAI (ESM)

For OpenAI, dynamic mode subscribes to the `window.openai.canvas.onToolResult` event for real-time updates:

```typescript
const result = await bundler.bundleToStaticHTML({
source: componentCode,
toolName: 'get_weather',
output: { temperature: 72 }, // Optional initial data
buildMode: 'dynamic',
dynamicOptions: {
includeInitialData: true, // Include initial data (default: true)
subscribeToUpdates: true, // Subscribe to events (default: true)
},
});
```

Generated HTML subscribes to OpenAI events:

```html
<script>
// Initial data (if includeInitialData: true)
window.__mcpToolOutput = {"temperature":72};

// Subscribe to updates
if (window.openai?.canvas?.onToolResult) {
window.openai.canvas.onToolResult(function(result) {
window.__mcpToolOutput = result;
window.__frontmcp.setState({ output: result, loading: false });
});
}
</script>
```

### Claude/Non-OpenAI (UMD)

For non-OpenAI platforms (Claude, etc.), dynamic mode uses **placeholders** since they can't subscribe to OpenAI events:

```html
<script>
window.__mcpToolOutput = "__FRONTMCP_OUTPUT_PLACEHOLDER__";
window.__mcpToolInput = "__FRONTMCP_INPUT_PLACEHOLDER__";

// Parse placeholders if replaced with JSON
if (window.__mcpToolOutput !== "__FRONTMCP_OUTPUT_PLACEHOLDER__") {
window.__mcpToolOutput = JSON.parse(window.__mcpToolOutput);
}
</script>
```

Use the `injectHybridDataFull` helper to replace placeholders before sending:

```typescript
import { injectHybridDataFull } from '@frontmcp/uipack/build';

// Build once
const shell = await bundler.bundleToStaticHTML({
source: componentCode,
toolName: 'get_weather',
buildMode: 'dynamic',
platform: 'claude',
});

// Inject data before sending
const html = injectHybridDataFull(
shell.html,
{ location: 'San Francisco' }, // input
{ temperature: 72, unit: 'F' }, // output
);
```

### Dynamic Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `includeInitialData` | `boolean` | `true` | Include initial data in HTML |
| `subscribeToUpdates` | `boolean` | `true` | Subscribe to platform events (OpenAI only) |

**Behavior when `includeInitialData: false`:**
- **OpenAI**: Shows loading state, waits for `onToolResult` event
- **Claude**: Shows loading state if placeholder not replaced, error if expected data missing

## Hybrid Mode

Hybrid mode creates a pre-built shell with placeholders that you replace at runtime. This is ideal for caching - build the shell once, inject different data per request.

```typescript
import { injectHybridData, injectHybridDataFull } from '@frontmcp/uipack/build';

// 1. Build shell ONCE at startup
const shell = await bundler.bundleToStaticHTML({
source: componentCode,
toolName: 'get_weather',
buildMode: 'hybrid',
});

// Cache the shell
const cachedShell = shell.html;

// 2. On each request, just inject data (no rebuild!)
const html1 = injectHybridDataFull(cachedShell, input1, output1);
const html2 = injectHybridDataFull(cachedShell, input2, output2);
```

### Placeholders

| Placeholder | Purpose |
|-------------|---------|
| `__FRONTMCP_OUTPUT_PLACEHOLDER__` | Replaced with tool output JSON |
| `__FRONTMCP_INPUT_PLACEHOLDER__` | Replaced with tool input JSON |

### Helper Functions

```typescript
import {
injectHybridData,
injectHybridDataFull,
isHybridShell,
HYBRID_DATA_PLACEHOLDER,
HYBRID_INPUT_PLACEHOLDER,
} from '@frontmcp/uipack/build';

// Inject output only
const html = injectHybridData(shell, { temperature: 72 });

// Inject both input and output
const html = injectHybridDataFull(shell, input, output);

// Check if HTML is a hybrid shell
if (isHybridShell(html)) {
// Contains placeholders - needs injection
}
```

### Error Handling

If placeholders are not replaced, the widget shows an error:

```javascript
// If placeholder not replaced:
window.__frontmcp.setState({
loading: false,
error: 'No data provided. The output placeholder was not replaced.'
});
```

## Multi-Platform Building

Build for multiple platforms at once with platform-specific behavior:

```typescript
const result = await bundler.bundleForMultiplePlatforms({
source: componentCode,
toolName: 'get_weather',
buildMode: 'dynamic',
platforms: ['openai', 'claude'],
});

// OpenAI HTML: subscribes to onToolResult events
const openaiHtml = result.platforms.openai.html;

// Claude HTML: has placeholders - inject data before sending
const claudeHtml = injectHybridDataFull(
result.platforms.claude.html,
input,
output,
);
```

## Platform Behavior Summary

| Mode | OpenAI (ESM) | Claude (UMD) |
|------|--------------|--------------|
| **static** | Data baked in | Data baked in |
| **dynamic** | Event subscription | Placeholders |
| **hybrid** | Placeholders | Placeholders |

## Best Practices

1. **Use static mode** for simple, one-off widgets
2. **Use dynamic mode** for multi-platform apps that need the same build mode everywhere
3. **Use hybrid mode** for high-performance scenarios where you cache the shell
4. **Always inject data** before sending hybrid/dynamic (Claude) HTML to clients

## TypeScript Types

```typescript
import type { BuildMode, DynamicModeOptions, HybridModeOptions } from '@frontmcp/ui/bundler';

type BuildMode = 'static' | 'dynamic' | 'hybrid';

interface DynamicModeOptions {
includeInitialData?: boolean; // default: true
subscribeToUpdates?: boolean; // default: true
}

interface HybridModeOptions {
placeholder?: string; // default: '__FRONTMCP_OUTPUT_PLACEHOLDER__'
inputPlaceholder?: string; // default: '__FRONTMCP_INPUT_PLACEHOLDER__'
}
```

## API Reference

### `bundleToStaticHTML(options)`

```typescript
interface StaticHTMLOptions {
// ... existing options ...

/** Build mode - controls data injection strategy */
buildMode?: BuildMode;

/** Options for dynamic mode */
dynamicOptions?: DynamicModeOptions;

/** Options for hybrid mode */
hybridOptions?: HybridModeOptions;
}

interface StaticHTMLResult {
// ... existing fields ...

/** The build mode used */
buildMode?: BuildMode;

/** Output placeholder (hybrid mode) */
dataPlaceholder?: string;

/** Input placeholder (hybrid mode) */
inputPlaceholder?: string;
}
```

### `injectHybridData(shell, data, placeholder?)`

Replaces the output placeholder with JSON data.

```typescript
function injectHybridData(
shell: string,
data: unknown,
placeholder?: string, // default: '__FRONTMCP_OUTPUT_PLACEHOLDER__'
): string;
```

### `injectHybridDataFull(shell, input, output)`

Replaces both input and output placeholders.

```typescript
function injectHybridDataFull(
shell: string,
input: unknown,
output: unknown,
): string;
```

### `isHybridShell(html, placeholder?)`

Checks if HTML contains the output placeholder.

```typescript
function isHybridShell(
html: string,
placeholder?: string,
): boolean;
```
17 changes: 17 additions & 0 deletions docs/draft/docs/ui/advanced/platforms.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@ description: FrontMCP UI adapts to different AI platform capabilities. Each plat
| **Gemini** | Limited | Inline preferred | Basic | JSON only |
| **generic-mcp** | Varies | CDN/Inline | inline, static | `_meta['ui/html']` |

## Build Modes & Data Injection

FrontMCP supports three build modes that behave differently per platform:

| Mode | OpenAI | Claude/Other |
|------|--------|--------------|
| **static** | Data baked in | Data baked in |
| **dynamic** | Event subscription | Placeholders |
| **hybrid** | Placeholders | Placeholders |

For OpenAI, **dynamic mode** subscribes to `window.openai.canvas.onToolResult` for real-time updates.
For Claude and other platforms, **dynamic mode** uses placeholders that must be replaced before sending.

<Card title="Build Modes Documentation" icon="hammer" href="/docs/ui/advanced/build-modes">
Learn about static, dynamic, and hybrid build modes with platform-aware data injection.
</Card>

## Platform Detection

FrontMCP automatically detects the platform:
Expand Down
2 changes: 0 additions & 2 deletions docs/draft/docs/ui/api-reference/theme.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,6 @@ import {
OPENAI_PLATFORM,
CLAUDE_PLATFORM,
GEMINI_PLATFORM,
NGROK_PLATFORM,
} from '@frontmcp/uipack/theme';
```

Expand Down Expand Up @@ -683,7 +682,6 @@ interface PlatformCapabilities {
| OpenAI | open | external | Yes | Yes | Yes |
| Claude | blocked | inline | Limited | No | No |
| Gemini | limited | inline | Limited | No | No |
| ngrok | open | external | Yes | Yes | Yes |

---

Expand Down
2 changes: 1 addition & 1 deletion docs/live/docs/guides/ui-library.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const loginForm = form(

## Theme and platform detection

Use `createTheme` to adjust palettes, typography, and CDN endpoints. Then decide whether to inline scripts (Claude Artifacts) or load from the network (OpenAI, Gemini, ngrok).
Use `createTheme` to adjust palettes, typography, and CDN endpoints. Then decide whether to inline scripts (Claude Artifacts) or load from the network (OpenAI, Gemini).

```ts
import {
Expand Down
Loading
Loading