Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 25 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
## [v0.6.2] - 2025-12-24

### feat

- Add static, dynamic, and hybrid UI build modes plus multi-platform bundling helpers so the same widget can hydrate differently on OpenAI, Claude, or Gemini without duplicate code.
- Auto-enable transport persistence whenever `redis` is configured, wiring session storage without needing a separate `transport.persistence` block.
- Teach `frontmcp build --adapter vercel` to detect npm, pnpm, yarn, or bun lockfiles, set the matching install/build commands, and emit Vercel Build Output API artifacts ready for deployment.

### fix

- Resolve dual-package hazards by lazily requiring `FrontMcpInstance` inside the decorator so runtime imports always reference the same module copy.
- Default primary-auth transport options now reuse `DEFAULT_TRANSPORT_CONFIG`, eliminating drift between schema defaults and runtime behavior.
- Serverless bundling loosens fully-specified import requirements, aliases optional React dependencies, and filters known rspack warnings so builds stay quiet but accurate.

### build

- All synchronized workspaces (sdk, cli, adapters, plugins) now publish dual CommonJS/ESM artifacts with `sideEffects: false` and shared typings for better tree-shaking.
- Independent packages `json-schema-to-zod-v3@1.0.3` and `mcp-from-openapi@2.1.1` match the new export map layout, ensuring adapters and downstream CLIs consume a single source of truth.

### docs

- Published the Build Modes guide plus a new callout on the platforms page to explain when to reach for static, dynamic, or hybrid rendering.
- Refreshed the live updates page with v0.6.2 highlights and links to the independent library releases.

## [v0.6.1] - 2025-12-22

### feat
Expand Down Expand Up @@ -91,7 +115,7 @@

### feat

- Publish the standalone `mcp-from-openapi` generator and wire the OpenAPI adapter to it so every tool inherits request mappers, conflict-free schemas, and per-scheme authentication analysis.
- Publish the standalone `mcp-from-openapi` generator and wire the OpenAPI adapter to it so every tool inherits conflict-free params, request mappers, and per-scheme authentication analysis.
- Allow `@Tool` metadata to declare literal primitives, tuple-style arrays, and MCP resources (plus `rawInputSchema`) so clients get typed responses without wrapping outputs in placeholder objects.
- Add a typed MCP error hierarchy and error handler so transports emit traceable IDs, consistent public/internal messages, and FlowControl-aware stop semantics.
- Extract `json-schema-to-zod-v3` with built-in regex guards so adapters and apps can reuse the hardened JSON Schema → Zod converter.
Expand Down
323 changes: 323 additions & 0 deletions docs/live/docs/ui/advanced/build-modes.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
---
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;
```
Loading