|
| 1 | +--- |
| 2 | +title: Build Modes |
| 3 | +sidebarTitle: Build Modes |
| 4 | +icon: hammer |
| 5 | +description: Control how widget data is injected into HTML - static baking, dynamic subscription, or hybrid placeholder injection. |
| 6 | +--- |
| 7 | + |
| 8 | +## Overview |
| 9 | + |
| 10 | +FrontMCP supports three build modes that control how tool input/output data is injected into the rendered HTML: |
| 11 | + |
| 12 | +| Mode | Description | Best For | |
| 13 | +| ----------- | ---------------------------------------------------------------------------- | --------------------------------- | |
| 14 | +| **static** | Data baked into HTML at build time | Default, simple widgets | |
| 15 | +| **dynamic** | Platform-aware - subscribes to events (OpenAI) or uses placeholders (Claude) | Real-time updates, multi-platform | |
| 16 | +| **hybrid** | Pre-built shell with placeholders, replace at runtime | Caching, high-performance | |
| 17 | + |
| 18 | +## Static Mode (Default) |
| 19 | + |
| 20 | +Data is serialized and embedded directly in the HTML at build time. |
| 21 | + |
| 22 | +```typescript |
| 23 | +const result = await bundler.bundleToStaticHTML({ |
| 24 | + source: componentCode, |
| 25 | + toolName: 'get_weather', |
| 26 | + output: { temperature: 72, unit: 'F' }, |
| 27 | + buildMode: 'static', // default |
| 28 | +}); |
| 29 | +``` |
| 30 | + |
| 31 | +The generated HTML includes the data inline: |
| 32 | + |
| 33 | +```html |
| 34 | +<script> |
| 35 | + window.__mcpToolOutput = {"temperature":72,"unit":"F"}; |
| 36 | + window.__frontmcp.setState({ |
| 37 | + toolName: "get_weather", |
| 38 | + output: window.__mcpToolOutput, |
| 39 | + loading: false, |
| 40 | + error: null |
| 41 | + }); |
| 42 | +</script> |
| 43 | +``` |
| 44 | + |
| 45 | +**Use when:** |
| 46 | + |
| 47 | +- Widget content doesn't change after initial render |
| 48 | +- Simple tool responses |
| 49 | +- No caching requirements |
| 50 | + |
| 51 | +## Dynamic Mode |
| 52 | + |
| 53 | +Dynamic mode is **platform-aware** - it behaves differently depending on the target platform: |
| 54 | + |
| 55 | +### OpenAI (ESM) |
| 56 | + |
| 57 | +For OpenAI, dynamic mode subscribes to the `window.openai.canvas.onToolResult` event for real-time updates: |
| 58 | + |
| 59 | +```typescript |
| 60 | +const result = await bundler.bundleToStaticHTML({ |
| 61 | + source: componentCode, |
| 62 | + toolName: 'get_weather', |
| 63 | + output: { temperature: 72 }, // Optional initial data |
| 64 | + buildMode: 'dynamic', |
| 65 | + dynamicOptions: { |
| 66 | + includeInitialData: true, // Include initial data (default: true) |
| 67 | + subscribeToUpdates: true, // Subscribe to events (default: true) |
| 68 | + }, |
| 69 | +}); |
| 70 | +``` |
| 71 | + |
| 72 | +Generated HTML subscribes to OpenAI events: |
| 73 | + |
| 74 | +```html |
| 75 | +<script> |
| 76 | + // Initial data (if includeInitialData: true) |
| 77 | + window.__mcpToolOutput = {"temperature":72}; |
| 78 | +
|
| 79 | + // Subscribe to updates |
| 80 | + if (window.openai?.canvas?.onToolResult) { |
| 81 | + window.openai.canvas.onToolResult(function(result) { |
| 82 | + window.__mcpToolOutput = result; |
| 83 | + window.__frontmcp.setState({ output: result, loading: false }); |
| 84 | + }); |
| 85 | + } |
| 86 | +</script> |
| 87 | +``` |
| 88 | +
|
| 89 | +### Claude/Non-OpenAI (UMD) |
| 90 | +
|
| 91 | +For non-OpenAI platforms (Claude, etc.), dynamic mode uses **placeholders** since they can't subscribe to OpenAI events: |
| 92 | +
|
| 93 | +```html |
| 94 | +<script> |
| 95 | + window.__mcpToolOutput = "__FRONTMCP_OUTPUT_PLACEHOLDER__"; |
| 96 | + window.__mcpToolInput = "__FRONTMCP_INPUT_PLACEHOLDER__"; |
| 97 | +
|
| 98 | + // Parse placeholders if replaced with JSON |
| 99 | + if (window.__mcpToolOutput !== "__FRONTMCP_OUTPUT_PLACEHOLDER__") { |
| 100 | + window.__mcpToolOutput = JSON.parse(window.__mcpToolOutput); |
| 101 | + } |
| 102 | +</script> |
| 103 | +``` |
| 104 | +
|
| 105 | +Use the `injectHybridDataFull` helper to replace placeholders before sending: |
| 106 | +
|
| 107 | +```typescript |
| 108 | +import { injectHybridDataFull } from '@frontmcp/uipack/build'; |
| 109 | +
|
| 110 | +// Build once |
| 111 | +const shell = await bundler.bundleToStaticHTML({ |
| 112 | + source: componentCode, |
| 113 | + toolName: 'get_weather', |
| 114 | + buildMode: 'dynamic', |
| 115 | + platform: 'claude', |
| 116 | +}); |
| 117 | +
|
| 118 | +// Inject data before sending |
| 119 | +const html = injectHybridDataFull( |
| 120 | + shell.html, |
| 121 | + { location: 'San Francisco' }, // input |
| 122 | + { temperature: 72, unit: 'F' }, // output |
| 123 | +); |
| 124 | +``` |
| 125 | +
|
| 126 | +### Dynamic Options |
| 127 | +
|
| 128 | +| Option | Type | Default | Description | |
| 129 | +| -------------------- | --------- | ------- | ------------------------------------------ | |
| 130 | +| `includeInitialData` | `boolean` | `true` | Include initial data in HTML | |
| 131 | +| `subscribeToUpdates` | `boolean` | `true` | Subscribe to platform events (OpenAI only) | |
| 132 | +
|
| 133 | +**Behavior when `includeInitialData: false`:** |
| 134 | +
|
| 135 | +- **OpenAI**: Shows loading state, waits for `onToolResult` event |
| 136 | +- **Claude**: Shows loading state if placeholder not replaced, error if expected data missing |
| 137 | +
|
| 138 | +## Hybrid Mode |
| 139 | +
|
| 140 | +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. |
| 141 | +
|
| 142 | +```typescript |
| 143 | +import { injectHybridData, injectHybridDataFull } from '@frontmcp/uipack/build'; |
| 144 | +
|
| 145 | +// 1. Build shell ONCE at startup |
| 146 | +const shell = await bundler.bundleToStaticHTML({ |
| 147 | + source: componentCode, |
| 148 | + toolName: 'get_weather', |
| 149 | + buildMode: 'hybrid', |
| 150 | +}); |
| 151 | +
|
| 152 | +// Cache the shell |
| 153 | +const cachedShell = shell.html; |
| 154 | +
|
| 155 | +// 2. On each request, just inject data (no rebuild!) |
| 156 | +const html1 = injectHybridDataFull(cachedShell, input1, output1); |
| 157 | +const html2 = injectHybridDataFull(cachedShell, input2, output2); |
| 158 | +``` |
| 159 | +
|
| 160 | +### Placeholders |
| 161 | +
|
| 162 | +| Placeholder | Purpose | |
| 163 | +| --------------------------------- | ------------------------------ | |
| 164 | +| `__FRONTMCP_OUTPUT_PLACEHOLDER__` | Replaced with tool output JSON | |
| 165 | +| `__FRONTMCP_INPUT_PLACEHOLDER__` | Replaced with tool input JSON | |
| 166 | +
|
| 167 | +### Helper Functions |
| 168 | +
|
| 169 | +```typescript |
| 170 | +import { |
| 171 | + injectHybridData, |
| 172 | + injectHybridDataFull, |
| 173 | + isHybridShell, |
| 174 | + HYBRID_DATA_PLACEHOLDER, |
| 175 | + HYBRID_INPUT_PLACEHOLDER, |
| 176 | +} from '@frontmcp/uipack/build'; |
| 177 | +
|
| 178 | +// Inject output only |
| 179 | +const html = injectHybridData(shell, { temperature: 72 }); |
| 180 | +
|
| 181 | +// Inject both input and output |
| 182 | +const html = injectHybridDataFull(shell, input, output); |
| 183 | +
|
| 184 | +// Check if HTML is a hybrid shell |
| 185 | +if (isHybridShell(html)) { |
| 186 | + // Contains placeholders - needs injection |
| 187 | +} |
| 188 | +``` |
| 189 | +
|
| 190 | +### Error Handling |
| 191 | +
|
| 192 | +If placeholders are not replaced, the widget shows an error: |
| 193 | +
|
| 194 | +```javascript |
| 195 | +// If placeholder not replaced: |
| 196 | +window.__frontmcp.setState({ |
| 197 | + loading: false, |
| 198 | + error: 'No data provided. The output placeholder was not replaced.' |
| 199 | +}); |
| 200 | +``` |
| 201 | +
|
| 202 | +## Multi-Platform Building |
| 203 | +
|
| 204 | +Build for multiple platforms at once with platform-specific behavior: |
| 205 | +
|
| 206 | +```typescript |
| 207 | +const result = await bundler.bundleForMultiplePlatforms({ |
| 208 | + source: componentCode, |
| 209 | + toolName: 'get_weather', |
| 210 | + buildMode: 'dynamic', |
| 211 | + platforms: ['openai', 'claude'], |
| 212 | +}); |
| 213 | +
|
| 214 | +// OpenAI HTML: subscribes to onToolResult events |
| 215 | +const openaiHtml = result.platforms.openai.html; |
| 216 | +
|
| 217 | +// Claude HTML: has placeholders - inject data before sending |
| 218 | +const claudeHtml = injectHybridDataFull( |
| 219 | + result.platforms.claude.html, |
| 220 | + input, |
| 221 | + output, |
| 222 | +); |
| 223 | +``` |
| 224 | +
|
| 225 | +## Platform Behavior Summary |
| 226 | +
|
| 227 | +| Mode | OpenAI (ESM) | Claude (UMD) | |
| 228 | +| ----------- | ------------------ | ------------- | |
| 229 | +| **static** | Data baked in | Data baked in | |
| 230 | +| **dynamic** | Event subscription | Placeholders | |
| 231 | +| **hybrid** | Placeholders | Placeholders | |
| 232 | +
|
| 233 | +## Best Practices |
| 234 | +
|
| 235 | +1. **Use static mode** for simple, one-off widgets |
| 236 | +2. **Use dynamic mode** for multi-platform apps that need the same build mode everywhere |
| 237 | +3. **Use hybrid mode** for high-performance scenarios where you cache the shell |
| 238 | +4. **Always inject data** before sending hybrid/dynamic (Claude) HTML to clients |
| 239 | +
|
| 240 | +## TypeScript Types |
| 241 | +
|
| 242 | +```typescript |
| 243 | +import type { BuildMode, DynamicModeOptions, HybridModeOptions } from '@frontmcp/ui/bundler'; |
| 244 | +
|
| 245 | +type BuildMode = 'static' | 'dynamic' | 'hybrid'; |
| 246 | +
|
| 247 | +interface DynamicModeOptions { |
| 248 | + includeInitialData?: boolean; // default: true |
| 249 | + subscribeToUpdates?: boolean; // default: true |
| 250 | +} |
| 251 | +
|
| 252 | +interface HybridModeOptions { |
| 253 | + placeholder?: string; // default: '__FRONTMCP_OUTPUT_PLACEHOLDER__' |
| 254 | + inputPlaceholder?: string; // default: '__FRONTMCP_INPUT_PLACEHOLDER__' |
| 255 | +} |
| 256 | +``` |
| 257 | +
|
| 258 | +## API Reference |
| 259 | +
|
| 260 | +### `bundleToStaticHTML(options)` |
| 261 | +
|
| 262 | +```typescript |
| 263 | +interface StaticHTMLOptions { |
| 264 | + // ... existing options ... |
| 265 | +
|
| 266 | + /** Build mode - controls data injection strategy */ |
| 267 | + buildMode?: BuildMode; |
| 268 | +
|
| 269 | + /** Options for dynamic mode */ |
| 270 | + dynamicOptions?: DynamicModeOptions; |
| 271 | +
|
| 272 | + /** Options for hybrid mode */ |
| 273 | + hybridOptions?: HybridModeOptions; |
| 274 | +} |
| 275 | +
|
| 276 | +interface StaticHTMLResult { |
| 277 | + // ... existing fields ... |
| 278 | +
|
| 279 | + /** The build mode used */ |
| 280 | + buildMode?: BuildMode; |
| 281 | +
|
| 282 | + /** Output placeholder (hybrid mode) */ |
| 283 | + dataPlaceholder?: string; |
| 284 | +
|
| 285 | + /** Input placeholder (hybrid mode) */ |
| 286 | + inputPlaceholder?: string; |
| 287 | +} |
| 288 | +``` |
| 289 | +
|
| 290 | +### `injectHybridData(shell, data, placeholder?)` |
| 291 | +
|
| 292 | +Replaces the output placeholder with JSON data. |
| 293 | +
|
| 294 | +```typescript |
| 295 | +function injectHybridData( |
| 296 | + shell: string, |
| 297 | + data: unknown, |
| 298 | + placeholder?: string, // default: '__FRONTMCP_OUTPUT_PLACEHOLDER__' |
| 299 | +): string; |
| 300 | +``` |
| 301 | +
|
| 302 | +### `injectHybridDataFull(shell, input, output)` |
| 303 | +
|
| 304 | +Replaces both input and output placeholders. |
| 305 | +
|
| 306 | +```typescript |
| 307 | +function injectHybridDataFull( |
| 308 | + shell: string, |
| 309 | + input: unknown, |
| 310 | + output: unknown, |
| 311 | +): string; |
| 312 | +``` |
| 313 | +
|
| 314 | +### `isHybridShell(html, placeholder?)` |
| 315 | +
|
| 316 | +Checks if HTML contains the output placeholder. |
| 317 | +
|
| 318 | +```typescript |
| 319 | +function isHybridShell( |
| 320 | + html: string, |
| 321 | + placeholder?: string, |
| 322 | +): boolean; |
| 323 | +``` |
0 commit comments