diff --git a/CHANGELOG.md b/CHANGELOG.md index 18bb81cc..0b4ad0b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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. diff --git a/docs/live/docs/ui/advanced/build-modes.mdx b/docs/live/docs/ui/advanced/build-modes.mdx new file mode 100644 index 00000000..805c3d7a --- /dev/null +++ b/docs/live/docs/ui/advanced/build-modes.mdx @@ -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 + +``` + +**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 + +``` + +### Claude/Non-OpenAI (UMD) + +For non-OpenAI platforms (Claude, etc.), dynamic mode uses **placeholders** since they can't subscribe to OpenAI events: + +```html + +``` + +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; +``` diff --git a/docs/live/docs/ui/advanced/platforms.mdx b/docs/live/docs/ui/advanced/platforms.mdx index eea33bd3..67814c34 100644 --- a/docs/live/docs/ui/advanced/platforms.mdx +++ b/docs/live/docs/ui/advanced/platforms.mdx @@ -7,16 +7,33 @@ description: FrontMCP UI adapts to different AI platform capabilities. Each plat ## Platform Capabilities -| 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']` | +| 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']` | + +## 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. + + + Learn about static, dynamic, and hybrid build modes with platform-aware data injection. + ## Platform Detection @@ -106,14 +123,14 @@ Claude uses Artifacts with restricted capabilities and a special **dual-payload* When `servingMode: 'auto'` detects a Claude client, FrontMCP returns a special two-block response: -```json +````json { "content": [ { "type": "text", "text": "{\"temperature\":72,\"unit\":\"F\"}" }, { "type": "text", "text": "Here is the visual result:\n\n```html\n...\n```" } ] } -``` +```` - **Block 0**: Pure JSON data for programmatic parsing - **Block 1**: Markdown-wrapped HTML that Claude displays as an Artifact @@ -124,7 +141,7 @@ Claude automatically detects the `html` code fence and offers to render it as an Control the text shown before the HTML block: -```typescript +````typescript @Tool({ name: 'get_weather', ui: { @@ -133,7 +150,7 @@ Control the text shown before the HTML block: }, }) // Output: "Here is the weather dashboard:\n\n```html\n..." -``` +```` Default: `'Here is the visual result'` diff --git a/docs/live/docs/ui/getting-started.mdx b/docs/live/docs/ui/getting-started.mdx index aadcb15d..69ce55e1 100644 --- a/docs/live/docs/ui/getting-started.mdx +++ b/docs/live/docs/ui/getting-started.mdx @@ -14,10 +14,10 @@ description: This guide walks you through adding visual widgets to your FrontMCP FrontMCP UI is split into two packages: -| Package | Use Case | -|---------|----------| +| Package | Use Case | +| ------------------ | ----------------------------------------------- | | `@frontmcp/uipack` | HTML components, build tools, themes (no React) | -| `@frontmcp/ui` | React components, hooks, SSR rendering | +| `@frontmcp/ui` | React components, hooks, SSR rendering | ```bash npm @@ -25,8 +25,10 @@ FrontMCP UI is split into two packages: npm install @frontmcp/uipack # For React templates + npm install @frontmcp/ui react react-dom -``` + +```` ```bash yarn # For HTML templates only (no React required) @@ -34,7 +36,7 @@ yarn add @frontmcp/uipack # For React templates yarn add @frontmcp/ui react react-dom -``` +```` ```bash pnpm # For HTML templates only (no React required) diff --git a/docs/live/docs/ui/integration/tools.mdx b/docs/live/docs/ui/integration/tools.mdx index a6f19213..a6e22e70 100644 --- a/docs/live/docs/ui/integration/tools.mdx +++ b/docs/live/docs/ui/integration/tools.mdx @@ -147,12 +147,12 @@ ui: { When `servingMode: 'auto'` (default), FrontMCP automatically selects the delivery method based on the client: -| Platform | Effective Mode | Response Format | -|----------|---------------|-----------------| -| OpenAI / ext-apps | `inline` | HTML in `_meta['ui/html']` | -| Claude | `inline` | Dual-payload (JSON + markdown-wrapped HTML) | -| Cursor | `inline` | HTML in `_meta['ui/html']` | -| Gemini / Unknown | (skipped) | JSON only | +| Platform | Effective Mode | Response Format | +| ----------------- | -------------- | ------------------------------------------- | +| OpenAI / ext-apps | `inline` | HTML in `_meta['ui/html']` | +| Claude | `inline` | Dual-payload (JSON + markdown-wrapped HTML) | +| Cursor | `inline` | HTML in `_meta['ui/html']` | +| Gemini / Unknown | (skipped) | JSON only | If `'auto'` mode detects an unsupported client, UI rendering is skipped entirely. The tool returns JSON-only data to avoid broken widget experiences. @@ -162,13 +162,13 @@ If `'auto'` mode detects an unsupported client, UI rendering is skipped entirely Customize the text shown before HTML in dual-payload responses (Claude): -```typescript +````typescript ui: { template: WeatherWidget, htmlResponsePrefix: 'Here is the weather dashboard', } // Claude output: "Here is the weather dashboard:\n\n```html\n...\n```" -``` +```` Default prefix is `'Here is the visual result'`. diff --git a/docs/live/docs/ui/overview.mdx b/docs/live/docs/ui/overview.mdx index 22f877da..c99392a6 100644 --- a/docs/live/docs/ui/overview.mdx +++ b/docs/live/docs/ui/overview.mdx @@ -9,10 +9,10 @@ description: A platform-agnostic component library for building rich HTML widget FrontMCP UI is split into two packages to support different use cases: -| Package | Purpose | React Required | -|---------|---------|----------------| -| **@frontmcp/uipack** | HTML components, build tools, themes, platform adapters | No | -| **@frontmcp/ui** | React components, hooks, SSR rendering | Yes | +| Package | Purpose | React Required | +| -------------------- | ------------------------------------------------------- | -------------- | +| **@frontmcp/uipack** | HTML components, build tools, themes, platform adapters | No | +| **@frontmcp/ui** | React components, hooks, SSR rendering | Yes | Use `@frontmcp/uipack` when you only need HTML string components or build tools. Use `@frontmcp/ui` when you need React components or hooks. @@ -68,13 +68,13 @@ When your MCP tools return data, AI platforms typically display it as raw text o FrontMCP UI adapts to different AI platform capabilities: -| Platform | Network | Widget Display | Response Format | -|----------|---------|----------------|-----------------| -| **OpenAI** | Full | Inline, Fullscreen, PiP | `_meta['ui/html']` | -| **ext-apps** | Full | Inline, Fullscreen, PiP | `_meta['ui/html']` | -| **Cursor** | Full | Inline | `_meta['ui/html']` | -| **Claude** | Blocked | Artifacts | Dual-payload | -| **Gemini** | Limited | Basic | JSON only | +| Platform | Network | Widget Display | Response Format | +| ------------ | ------- | ----------------------- | ------------------ | +| **OpenAI** | Full | Inline, Fullscreen, PiP | `_meta['ui/html']` | +| **ext-apps** | Full | Inline, Fullscreen, PiP | `_meta['ui/html']` | +| **Cursor** | Full | Inline | `_meta['ui/html']` | +| **Claude** | Blocked | Artifacts | Dual-payload | +| **Gemini** | Limited | Basic | JSON only | The library automatically detects the platform and selects the appropriate delivery format. @@ -110,14 +110,14 @@ With `servingMode: 'auto'` (default), unsupported platforms automatically receiv Every `@Tool` exposes a `ui` block that describes how the widget should render across hosts. Configure these settings once and the renderer keeps your UX consistent everywhere. -| Setting | Controls | Use it when | -| ------------------ | -------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| `displayMode` | Whether the widget renders inline, fullscreen, or picture-in-picture | Highlight dashboards inline or promote immersive flows into fullscreen/PiP. | -| `servingMode` | How HTML is delivered (`auto`, `inline`, `static`, `hybrid`, URL-based). Default: `'auto'` | Let FrontMCP select the best format per platform, or force a specific mode. | -| `htmlResponsePrefix` | Text shown before HTML in Claude's dual-payload format | Customize the artifact description for better Claude UX. | -| `widgetAccessible` | Allows widgets to call tools through the MCP Bridge | Create follow-up actions (retry, escalate, schedule) directly from the UI. | -| `csp` | Allowed domains for scripts, images, fonts, and network calls | Restrict remote dependencies per platform and satisfy Claude's strict CSP. | -| `hydrate` | Ships the React runtime for client-side interactivity | Power forms, tabs, and charts that need local state or event handlers. | +| Setting | Controls | Use it when | +| -------------------- | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | +| `displayMode` | Whether the widget renders inline, fullscreen, or picture-in-picture | Highlight dashboards inline or promote immersive flows into fullscreen/PiP. | +| `servingMode` | How HTML is delivered (`auto`, `inline`, `static`, `hybrid`, URL-based). Default: `'auto'` | Let FrontMCP select the best format per platform, or force a specific mode. | +| `htmlResponsePrefix` | Text shown before HTML in Claude's dual-payload format | Customize the artifact description for better Claude UX. | +| `widgetAccessible` | Allows widgets to call tools through the MCP Bridge | Create follow-up actions (retry, escalate, schedule) directly from the UI. | +| `csp` | Allowed domains for scripts, images, fonts, and network calls | Restrict remote dependencies per platform and satisfy Claude's strict CSP. | +| `hydrate` | Ships the React runtime for client-side interactivity | Power forms, tabs, and charts that need local state or event handlers. | Pair metadata with a concise `widgetDescription` so clients can preview the UI before rendering it, just like Mintlify recommends leading with the most important context. @@ -222,8 +222,10 @@ export class GetWeatherTool extends ToolContext { npm install @frontmcp/uipack # For React components and hooks + npm install @frontmcp/ui react react-dom -``` + +```` ```bash yarn # For HTML components only (no React) @@ -231,7 +233,7 @@ yarn add @frontmcp/uipack # For React components and hooks yarn add @frontmcp/ui react react-dom -``` +```` ```bash pnpm # For HTML components only (no React) diff --git a/docs/live/updates.mdx b/docs/live/updates.mdx index 8da467a2..e5dce59c 100644 --- a/docs/live/updates.mdx +++ b/docs/live/updates.mdx @@ -5,6 +5,53 @@ icon: 'sparkles' mode: 'center' --- + + + 🚀 **Build modes everywhere** – Bundle widgets in static, dynamic, or hybrid mode and reuse the same component across OpenAI, Claude, and Gemini without rewriting HTML. + + 🧩 **Auto Redis persistence** – Point `redis` at a store once and transport persistence now enables itself so stateful sessions survive restarts and serverless cold starts. + + ⚡ **Vercel Build Output** – `frontmcp build --adapter vercel` detects npm/yarn/pnpm/bun lockfiles, wires the right install/build commands, and emits a `.vercel/output` tree ready for `vercel deploy --prebuilt`. + + 🛠️ **Tree-shakable packages** – SDK, adapters, plugins, and CLI publish dual CJS/ESM entries with `sideEffects: false`, eliminating duplicate React imports and keeping bundles slim. + + + + + + + 🧱 **Dual module builds** – Import from ESM or require from CommonJS via the new `exports` map while sharing a single `dist/index.d.ts`. + + ⚡ **Side-effect free** – `sideEffects: false` and flattened `dist/esm` output let bundlers drop unused converters automatically. + + 🔧 **Workspace alignment** – Published metadata now mirrors the FrontMCP 0.6.2 toolchain so CLI scaffolds and adapters consume the converter without overrides. + + + + + + + 🧱 **Unified exports** – Require or import the generator thanks to the new dual CJS/ESM build, with typings shared across both entry points. + + ⚙️ **Tree-shakable helpers** – Marking the package as side-effect free keeps bundlers from pulling the OpenAPI loader when you only need schema utilities. + + 🔄 **Version sync** – The library now ships in lockstep with `@frontmcp/adapters`, so generated tools inherit the same input mappers and security metadata. + + + + (); async getTools(apiUrl: string): Promise { - if (this.cache.has(apiUrl)) { + if (this.cache has(apiUrl)) { return this.cache.get(apiUrl)!; }