diff --git a/examples/appssdk-adapter-demo/README.md b/examples/appssdk-adapter-demo/README.md
new file mode 100644
index 00000000..f4a22dd6
--- /dev/null
+++ b/examples/appssdk-adapter-demo/README.md
@@ -0,0 +1,242 @@
+# Apps SDK Adapter Demo
+
+This example demonstrates how to use platform adapters in `@mcp-ui/server` to make MCP-UI widgets compatible with different environments. Specifically, this shows the Apps SDK adapter for environments like ChatGPT.
+
+## What are Adapters?
+
+Adapters enable MCP-UI widgets to work seamlessly across different platform environments by:
+- Translating MCP-UI `postMessage` calls to platform-specific API calls (e.g., `window.openai` in ChatGPT)
+- Handling bidirectional communication (tools, prompts, state management)
+- Working transparently without requiring changes to your existing MCP-UI code
+
+The adapters architecture supports multiple platforms, with Apps SDK being the first example.
+
+## Example Code
+
+```typescript
+import { createUIResource } from '@mcp-ui/server';
+
+// Example 1: Simple button with tool call
+const simpleButtonResource = createUIResource({
+ uri: 'ui://appssdk-demo/button',
+ content: {
+ type: 'rawHtml',
+ htmlString: `
+
+
+
+
+
+
+ Apps SDK Adapter Demo
+ Call Tool
+
+
+
+
+
+ `
+ },
+ encoding: 'text',
+ // Enable adapters
+ adapters: {
+ appsSdk: {
+ enabled: true,
+ config: {
+ intentHandling: 'prompt',
+ timeout: 30000
+ }
+ }
+ }
+});
+
+// Example 2: Widget that uses render data from Apps SDK
+const renderDataResource = createUIResource({
+ uri: 'ui://appssdk-demo/render-data',
+ content: {
+ type: 'rawHtml',
+ htmlString: `
+
+
+
+
+
+
+ Render Data Demo
+ Loading...
+
+
+
+
+ `
+ },
+ encoding: 'text',
+ adapters: {
+ appsSdk: {
+ enabled: true
+ }
+ }
+});
+
+// Example 3: Interactive form with multiple actions
+const interactiveFormResource = createUIResource({
+ uri: 'ui://appssdk-demo/form',
+ content: {
+ type: 'rawHtml',
+ htmlString: `
+
+
+
+
+
+
+ Interactive Form
+
+
+ Send Prompt
+ Notify
+
+
+
+
+ `
+ },
+ encoding: 'text',
+ adapters: {
+ appsSdk: {
+ enabled: true,
+ config: {
+ intentHandling: 'prompt'
+ }
+ }
+ }
+});
+```
+
+## How to Use
+
+1. Install dependencies:
+```bash
+npm install @mcp-ui/server
+```
+
+2. Create your MCP server with the examples above
+
+3. Enable adapters by configuring the `adapters` field (e.g., `adapters.appsSdk.enabled: true`)
+
+4. Deploy your server and connect it to the target environment (e.g., ChatGPT)
+
+5. Your MCP-UI widgets will now work in that environment!
+
+## Supported Features
+
+✅ **Tool Calls** - Call tools using `{ type: 'tool', payload: { toolName, params } }`
+
+✅ **Prompts** - Send follow-up messages using `{ type: 'prompt', payload: { prompt } }`
+
+✅ **Intents** - Express user intents (automatically converted to prompts)
+
+✅ **Notifications** - Send notifications to the host
+
+✅ **Render Data** - Access context like `toolInput`, `toolOutput`, `theme`, `locale`
+
+⚠️ **Links** - Navigation may not be supported in all Apps SDK environments
+
+## Configuration Options
+
+```typescript
+adapters: {
+ appsSdk?: {
+ enabled: boolean, // Enable/disable the Apps SDK adapter
+ config?: {
+ intentHandling?: 'prompt' | 'ignore', // How to handle intent messages
+ timeout?: number, // Timeout for async operations (ms)
+ hostOrigin?: string // Origin for MessageEvents
+ }
+ }
+ // Future adapters can be added here
+ // anotherPlatform?: { ... }
+}
+```
+
+## Learn More
+
+- [Apps SDK Documentation](https://developers.openai.com/apps-sdk/)
+- [MCP-UI Documentation](https://mcpui.dev)
+
diff --git a/examples/appssdk-adapter-demo/example.ts b/examples/appssdk-adapter-demo/example.ts
new file mode 100644
index 00000000..8c9c6b0a
--- /dev/null
+++ b/examples/appssdk-adapter-demo/example.ts
@@ -0,0 +1,408 @@
+/**
+ * Platform Adapters Example
+ *
+ * This example demonstrates how to create MCP-UI resources that work
+ * seamlessly across multiple platforms using adapters. The Apps SDK adapter
+ * enables compatibility with environments like ChatGPT.
+ */
+
+import { createUIResource } from '@mcp-ui/server';
+
+// Example 1: Simple interactive button
+export const simpleButtonExample = createUIResource({
+ uri: 'ui://appssdk-demo/button',
+ content: {
+ type: 'rawHtml',
+ htmlString: `
+
+
+
+
+
+
+
+
+
+
🌤️ Weather Tool
+
Click the button to call the weather tool
+
Get Weather
+
+
+
+
+
+
+ `
+ },
+ encoding: 'text',
+ // Enable adapters
+ adapters: {
+ appsSdk: {
+ enabled: true,
+ config: {
+ intentHandling: 'prompt',
+ timeout: 30000
+ }
+ }
+ }
+});
+
+// Example 2: Accessing Apps SDK render data
+export const renderDataExample = createUIResource({
+ uri: 'ui://appssdk-demo/render-data',
+ content: {
+ type: 'rawHtml',
+ htmlString: `
+
+
+
+
+
+
+
+
+
+
📊 Apps SDK Context
+
Loading render data...
+
+
+
+
+
+ `
+ },
+ encoding: 'text',
+ adapters: {
+ appsSdk: {
+ enabled: true
+ }
+ }
+});
+
+// Example 3: Interactive form with multiple action types
+export const interactiveFormExample = createUIResource({
+ uri: 'ui://appssdk-demo/interactive-form',
+ content: {
+ type: 'rawHtml',
+ htmlString: `
+
+
+
+
+
+
+
+
+
+
💬 Interactive Demo
+
+
+
+
+ Send Prompt
+ Notify
+ Send Intent
+
+
+
+
+
+
+
+
+ `
+ },
+ encoding: 'text',
+ adapters: {
+ appsSdk: {
+ enabled: true,
+ config: {
+ intentHandling: 'prompt', // Intents will be converted to prompts
+ timeout: 30000
+ }
+ }
+ }
+});
+
+// Export all examples
+export const appsSdkAdapterExamples = {
+ simpleButton: simpleButtonExample,
+ renderData: renderDataExample,
+ interactiveForm: interactiveFormExample
+};
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cc152c0b..457387d1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -450,6 +450,9 @@ importers:
'@vitest/coverage-v8':
specifier: ^1.0.0
version: 1.6.1(vitest@1.6.1(@types/node@18.19.112)(jsdom@23.2.0)(lightningcss@1.30.1))
+ esbuild:
+ specifier: ^0.19.0
+ version: 0.19.12
typescript:
specifier: ^5.0.0
version: 5.8.3
@@ -967,6 +970,12 @@ packages:
'@emnapi/wasi-threads@1.0.2':
resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==}
+ '@esbuild/aix-ppc64@0.19.12':
+ resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [aix]
+
'@esbuild/aix-ppc64@0.21.5':
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
engines: {node: '>=12'}
@@ -985,6 +994,12 @@ packages:
cpu: [ppc64]
os: [aix]
+ '@esbuild/android-arm64@0.19.12':
+ resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+
'@esbuild/android-arm64@0.21.5':
resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
engines: {node: '>=12'}
@@ -1003,6 +1018,12 @@ packages:
cpu: [arm64]
os: [android]
+ '@esbuild/android-arm@0.19.12':
+ resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+
'@esbuild/android-arm@0.21.5':
resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
engines: {node: '>=12'}
@@ -1021,6 +1042,12 @@ packages:
cpu: [arm]
os: [android]
+ '@esbuild/android-x64@0.19.12':
+ resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+
'@esbuild/android-x64@0.21.5':
resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
engines: {node: '>=12'}
@@ -1039,6 +1066,12 @@ packages:
cpu: [x64]
os: [android]
+ '@esbuild/darwin-arm64@0.19.12':
+ resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+
'@esbuild/darwin-arm64@0.21.5':
resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
engines: {node: '>=12'}
@@ -1057,6 +1090,12 @@ packages:
cpu: [arm64]
os: [darwin]
+ '@esbuild/darwin-x64@0.19.12':
+ resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+
'@esbuild/darwin-x64@0.21.5':
resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
engines: {node: '>=12'}
@@ -1075,6 +1114,12 @@ packages:
cpu: [x64]
os: [darwin]
+ '@esbuild/freebsd-arm64@0.19.12':
+ resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+
'@esbuild/freebsd-arm64@0.21.5':
resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
engines: {node: '>=12'}
@@ -1093,6 +1138,12 @@ packages:
cpu: [arm64]
os: [freebsd]
+ '@esbuild/freebsd-x64@0.19.12':
+ resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+
'@esbuild/freebsd-x64@0.21.5':
resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
engines: {node: '>=12'}
@@ -1111,6 +1162,12 @@ packages:
cpu: [x64]
os: [freebsd]
+ '@esbuild/linux-arm64@0.19.12':
+ resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+
'@esbuild/linux-arm64@0.21.5':
resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
engines: {node: '>=12'}
@@ -1129,6 +1186,12 @@ packages:
cpu: [arm64]
os: [linux]
+ '@esbuild/linux-arm@0.19.12':
+ resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+
'@esbuild/linux-arm@0.21.5':
resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
engines: {node: '>=12'}
@@ -1147,6 +1210,12 @@ packages:
cpu: [arm]
os: [linux]
+ '@esbuild/linux-ia32@0.19.12':
+ resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+
'@esbuild/linux-ia32@0.21.5':
resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
engines: {node: '>=12'}
@@ -1165,6 +1234,12 @@ packages:
cpu: [ia32]
os: [linux]
+ '@esbuild/linux-loong64@0.19.12':
+ resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+
'@esbuild/linux-loong64@0.21.5':
resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
engines: {node: '>=12'}
@@ -1183,6 +1258,12 @@ packages:
cpu: [loong64]
os: [linux]
+ '@esbuild/linux-mips64el@0.19.12':
+ resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+
'@esbuild/linux-mips64el@0.21.5':
resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
engines: {node: '>=12'}
@@ -1201,6 +1282,12 @@ packages:
cpu: [mips64el]
os: [linux]
+ '@esbuild/linux-ppc64@0.19.12':
+ resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+
'@esbuild/linux-ppc64@0.21.5':
resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
engines: {node: '>=12'}
@@ -1219,6 +1306,12 @@ packages:
cpu: [ppc64]
os: [linux]
+ '@esbuild/linux-riscv64@0.19.12':
+ resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+
'@esbuild/linux-riscv64@0.21.5':
resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
engines: {node: '>=12'}
@@ -1237,6 +1330,12 @@ packages:
cpu: [riscv64]
os: [linux]
+ '@esbuild/linux-s390x@0.19.12':
+ resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+
'@esbuild/linux-s390x@0.21.5':
resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
engines: {node: '>=12'}
@@ -1255,6 +1354,12 @@ packages:
cpu: [s390x]
os: [linux]
+ '@esbuild/linux-x64@0.19.12':
+ resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+
'@esbuild/linux-x64@0.21.5':
resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
engines: {node: '>=12'}
@@ -1285,6 +1390,12 @@ packages:
cpu: [arm64]
os: [netbsd]
+ '@esbuild/netbsd-x64@0.19.12':
+ resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+
'@esbuild/netbsd-x64@0.21.5':
resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
engines: {node: '>=12'}
@@ -1315,6 +1426,12 @@ packages:
cpu: [arm64]
os: [openbsd]
+ '@esbuild/openbsd-x64@0.19.12':
+ resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+
'@esbuild/openbsd-x64@0.21.5':
resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
engines: {node: '>=12'}
@@ -1333,6 +1450,12 @@ packages:
cpu: [x64]
os: [openbsd]
+ '@esbuild/sunos-x64@0.19.12':
+ resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+
'@esbuild/sunos-x64@0.21.5':
resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
engines: {node: '>=12'}
@@ -1351,6 +1474,12 @@ packages:
cpu: [x64]
os: [sunos]
+ '@esbuild/win32-arm64@0.19.12':
+ resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+
'@esbuild/win32-arm64@0.21.5':
resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
engines: {node: '>=12'}
@@ -1369,6 +1498,12 @@ packages:
cpu: [arm64]
os: [win32]
+ '@esbuild/win32-ia32@0.19.12':
+ resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+
'@esbuild/win32-ia32@0.21.5':
resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
engines: {node: '>=12'}
@@ -1387,6 +1522,12 @@ packages:
cpu: [ia32]
os: [win32]
+ '@esbuild/win32-x64@0.19.12':
+ resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+
'@esbuild/win32-x64@0.21.5':
resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
engines: {node: '>=12'}
@@ -4460,6 +4601,11 @@ packages:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'}
+ esbuild@0.19.12:
+ resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==}
+ engines: {node: '>=12'}
+ hasBin: true
+
esbuild@0.21.5:
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
engines: {node: '>=12'}
@@ -8728,6 +8874,9 @@ snapshots:
tslib: 2.8.1
optional: true
+ '@esbuild/aix-ppc64@0.19.12':
+ optional: true
+
'@esbuild/aix-ppc64@0.21.5':
optional: true
@@ -8737,6 +8886,9 @@ snapshots:
'@esbuild/aix-ppc64@0.25.5':
optional: true
+ '@esbuild/android-arm64@0.19.12':
+ optional: true
+
'@esbuild/android-arm64@0.21.5':
optional: true
@@ -8746,6 +8898,9 @@ snapshots:
'@esbuild/android-arm64@0.25.5':
optional: true
+ '@esbuild/android-arm@0.19.12':
+ optional: true
+
'@esbuild/android-arm@0.21.5':
optional: true
@@ -8755,6 +8910,9 @@ snapshots:
'@esbuild/android-arm@0.25.5':
optional: true
+ '@esbuild/android-x64@0.19.12':
+ optional: true
+
'@esbuild/android-x64@0.21.5':
optional: true
@@ -8764,6 +8922,9 @@ snapshots:
'@esbuild/android-x64@0.25.5':
optional: true
+ '@esbuild/darwin-arm64@0.19.12':
+ optional: true
+
'@esbuild/darwin-arm64@0.21.5':
optional: true
@@ -8773,6 +8934,9 @@ snapshots:
'@esbuild/darwin-arm64@0.25.5':
optional: true
+ '@esbuild/darwin-x64@0.19.12':
+ optional: true
+
'@esbuild/darwin-x64@0.21.5':
optional: true
@@ -8782,6 +8946,9 @@ snapshots:
'@esbuild/darwin-x64@0.25.5':
optional: true
+ '@esbuild/freebsd-arm64@0.19.12':
+ optional: true
+
'@esbuild/freebsd-arm64@0.21.5':
optional: true
@@ -8791,6 +8958,9 @@ snapshots:
'@esbuild/freebsd-arm64@0.25.5':
optional: true
+ '@esbuild/freebsd-x64@0.19.12':
+ optional: true
+
'@esbuild/freebsd-x64@0.21.5':
optional: true
@@ -8800,6 +8970,9 @@ snapshots:
'@esbuild/freebsd-x64@0.25.5':
optional: true
+ '@esbuild/linux-arm64@0.19.12':
+ optional: true
+
'@esbuild/linux-arm64@0.21.5':
optional: true
@@ -8809,6 +8982,9 @@ snapshots:
'@esbuild/linux-arm64@0.25.5':
optional: true
+ '@esbuild/linux-arm@0.19.12':
+ optional: true
+
'@esbuild/linux-arm@0.21.5':
optional: true
@@ -8818,6 +8994,9 @@ snapshots:
'@esbuild/linux-arm@0.25.5':
optional: true
+ '@esbuild/linux-ia32@0.19.12':
+ optional: true
+
'@esbuild/linux-ia32@0.21.5':
optional: true
@@ -8827,6 +9006,9 @@ snapshots:
'@esbuild/linux-ia32@0.25.5':
optional: true
+ '@esbuild/linux-loong64@0.19.12':
+ optional: true
+
'@esbuild/linux-loong64@0.21.5':
optional: true
@@ -8836,6 +9018,9 @@ snapshots:
'@esbuild/linux-loong64@0.25.5':
optional: true
+ '@esbuild/linux-mips64el@0.19.12':
+ optional: true
+
'@esbuild/linux-mips64el@0.21.5':
optional: true
@@ -8845,6 +9030,9 @@ snapshots:
'@esbuild/linux-mips64el@0.25.5':
optional: true
+ '@esbuild/linux-ppc64@0.19.12':
+ optional: true
+
'@esbuild/linux-ppc64@0.21.5':
optional: true
@@ -8854,6 +9042,9 @@ snapshots:
'@esbuild/linux-ppc64@0.25.5':
optional: true
+ '@esbuild/linux-riscv64@0.19.12':
+ optional: true
+
'@esbuild/linux-riscv64@0.21.5':
optional: true
@@ -8863,6 +9054,9 @@ snapshots:
'@esbuild/linux-riscv64@0.25.5':
optional: true
+ '@esbuild/linux-s390x@0.19.12':
+ optional: true
+
'@esbuild/linux-s390x@0.21.5':
optional: true
@@ -8872,6 +9066,9 @@ snapshots:
'@esbuild/linux-s390x@0.25.5':
optional: true
+ '@esbuild/linux-x64@0.19.12':
+ optional: true
+
'@esbuild/linux-x64@0.21.5':
optional: true
@@ -8887,6 +9084,9 @@ snapshots:
'@esbuild/netbsd-arm64@0.25.5':
optional: true
+ '@esbuild/netbsd-x64@0.19.12':
+ optional: true
+
'@esbuild/netbsd-x64@0.21.5':
optional: true
@@ -8902,6 +9102,9 @@ snapshots:
'@esbuild/openbsd-arm64@0.25.5':
optional: true
+ '@esbuild/openbsd-x64@0.19.12':
+ optional: true
+
'@esbuild/openbsd-x64@0.21.5':
optional: true
@@ -8911,6 +9114,9 @@ snapshots:
'@esbuild/openbsd-x64@0.25.5':
optional: true
+ '@esbuild/sunos-x64@0.19.12':
+ optional: true
+
'@esbuild/sunos-x64@0.21.5':
optional: true
@@ -8920,6 +9126,9 @@ snapshots:
'@esbuild/sunos-x64@0.25.5':
optional: true
+ '@esbuild/win32-arm64@0.19.12':
+ optional: true
+
'@esbuild/win32-arm64@0.21.5':
optional: true
@@ -8929,6 +9138,9 @@ snapshots:
'@esbuild/win32-arm64@0.25.5':
optional: true
+ '@esbuild/win32-ia32@0.19.12':
+ optional: true
+
'@esbuild/win32-ia32@0.21.5':
optional: true
@@ -8938,6 +9150,9 @@ snapshots:
'@esbuild/win32-ia32@0.25.5':
optional: true
+ '@esbuild/win32-x64@0.19.12':
+ optional: true
+
'@esbuild/win32-x64@0.21.5':
optional: true
@@ -12610,6 +12825,32 @@ snapshots:
is-date-object: 1.1.0
is-symbol: 1.1.1
+ esbuild@0.19.12:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.19.12
+ '@esbuild/android-arm': 0.19.12
+ '@esbuild/android-arm64': 0.19.12
+ '@esbuild/android-x64': 0.19.12
+ '@esbuild/darwin-arm64': 0.19.12
+ '@esbuild/darwin-x64': 0.19.12
+ '@esbuild/freebsd-arm64': 0.19.12
+ '@esbuild/freebsd-x64': 0.19.12
+ '@esbuild/linux-arm': 0.19.12
+ '@esbuild/linux-arm64': 0.19.12
+ '@esbuild/linux-ia32': 0.19.12
+ '@esbuild/linux-loong64': 0.19.12
+ '@esbuild/linux-mips64el': 0.19.12
+ '@esbuild/linux-ppc64': 0.19.12
+ '@esbuild/linux-riscv64': 0.19.12
+ '@esbuild/linux-s390x': 0.19.12
+ '@esbuild/linux-x64': 0.19.12
+ '@esbuild/netbsd-x64': 0.19.12
+ '@esbuild/openbsd-x64': 0.19.12
+ '@esbuild/sunos-x64': 0.19.12
+ '@esbuild/win32-arm64': 0.19.12
+ '@esbuild/win32-ia32': 0.19.12
+ '@esbuild/win32-x64': 0.19.12
+
esbuild@0.21.5:
optionalDependencies:
'@esbuild/aix-ppc64': 0.21.5
diff --git a/sdks/typescript/server/README.md b/sdks/typescript/server/README.md
index b8ae03bc..b41a3750 100644
--- a/sdks/typescript/server/README.md
+++ b/sdks/typescript/server/README.md
@@ -143,6 +143,89 @@ Rendered using the internal ` ` component, which uti
UI snippets must be able to interact with the agent. In `mcp-ui`, this is done by hooking into events sent from the UI snippet and reacting to them in the host (see `onUIAction` prop). For example, an HTML may trigger a tool call when a button is clicked by sending an event which will be caught handled by the client.
+### Platform Adapters
+
+`@mcp-ui/server` includes adapter support for host-specific implementations, enabling your open MCP-UI widgets to work seamlessly regardless of host. Adapters automatically translate between MCP-UI's `postMessage` protocol and host-specific APIs. Over time, as hosts become compatible with the open spec, these adapters wouldn't be needed.
+
+#### Available Adapters
+
+##### Apps SDK Adapter
+
+For Apps SDK environments (e.g., ChatGPT), this adapter translates MCP-UI protocol to Apps SDK API calls (e.g., `window.openai`).
+
+**How it Works:**
+- Intercepts MCP-UI `postMessage` calls from your widgets
+- Translates them to appropriate Apps SDK API calls
+- Handles bidirectional communication (tools, prompts, state management)
+- Works transparently - your existing MCP-UI code continues to work without changes
+
+**Usage:**
+
+```ts
+import { createUIResource } from '@mcp-ui/server';
+
+const htmlResource = createUIResource({
+ uri: 'ui://greeting/1',
+ content: {
+ type: 'rawHtml',
+ htmlString: `
+
+ Call Tool
+
+ `
+ },
+ encoding: 'text',
+ // Enable adapters
+ adapters: {
+ appsSdk: {
+ enabled: true,
+ config: {
+ intentHandling: 'prompt', // or 'ignore'
+ timeout: 30000, // optional, in milliseconds
+ }
+ }
+ // Future adapters can be enabled here
+ // anotherPlatform: { enabled: true }
+ }
+});
+```
+
+The adapter scripts are automatically injected into your HTML content and handle all protocol translation.
+
+**Supported Actions:**
+- ✅ **Tool calls** - `{ type: 'tool', payload: { toolName, params } }`
+- ✅ **Prompts** - `{ type: 'prompt', payload: { prompt } }`
+- ✅ **Intents** - `{ type: 'intent', payload: { intent, params } }` (converted to prompts)
+- ✅ **Notifications** - `{ type: 'notify', payload: { message } }`
+- ✅ **Render data** - Access to `toolInput`, `toolOutput`, `widgetState`, `theme`, `locale`
+- ⚠️ **Links** - `{ type: 'link', payload: { url } }` (may not be supported, returns error in some environments)
+
+#### Advanced Usage
+
+You can manually wrap HTML with adapters or access adapter scripts directly:
+
+```ts
+import { wrapHtmlWithAdapters, getAppsSdkAdapterScript } from '@mcp-ui/server';
+
+// Manually wrap HTML with adapters
+const wrappedHtml = wrapHtmlWithAdapters(
+ 'Click me ',
+ {
+ appsSdk: {
+ enabled: true,
+ config: { intentHandling: 'ignore' }
+ }
+ }
+);
+
+// Get a specific adapter script
+const appsSdkScript = getAppsSdkAdapterScript({ timeout: 60000 });
+```
+
+#### Future Adapters
+
+The adapters architecture is designed to support multiple platforms. Future adapters can be added to the `adapters` configuration object as they become available.
+
## 🏗️ Installation
### TypeScript
diff --git a/sdks/typescript/server/package.json b/sdks/typescript/server/package.json
index d4f948fe..201c72f0 100644
--- a/sdks/typescript/server/package.json
+++ b/sdks/typescript/server/package.json
@@ -20,6 +20,7 @@
"devDependencies": {
"@types/node": "^18.19.100",
"@vitest/coverage-v8": "^1.0.0",
+ "esbuild": "^0.19.0",
"typescript": "^5.0.0",
"vite": "^5.0.0",
"vite-plugin-dts": "^3.6.0",
@@ -27,6 +28,8 @@
},
"scripts": {
"prepublishOnly": "pnpm run build",
+ "prebuild": "pnpm run bundle:adapter",
+ "bundle:adapter": "node scripts/bundle-adapter.js",
"dev": "vite",
"build": "vite build",
"test": "vitest run",
diff --git a/sdks/typescript/server/scripts/bundle-adapter.js b/sdks/typescript/server/scripts/bundle-adapter.js
new file mode 100644
index 00000000..93882ccf
--- /dev/null
+++ b/sdks/typescript/server/scripts/bundle-adapter.js
@@ -0,0 +1,41 @@
+import { build } from 'esbuild';
+import { writeFileSync } from 'fs';
+import { fileURLToPath } from 'url';
+import { dirname, join } from 'path';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+// Bundle the adapter runtime directly from the adapter-runtime.ts file
+try {
+ const result = await build({
+ entryPoints: [join(__dirname, '../src/adapters/appssdk/adapter-runtime.ts')],
+ bundle: true,
+ write: false,
+ format: 'iife',
+ platform: 'browser',
+ target: 'es2020',
+ minify: false, // Keep readable for debugging
+ });
+
+ const bundledCode = result.outputFiles[0].text;
+
+ // Write the bundled code to a TypeScript file
+ // Escape backslashes first, then backticks and template literal expressions
+ const escapedCode = bundledCode
+ .replace(/\\/g, '\\\\') // Escape backslashes first
+ .replace(/`/g, '\\`') // Then escape backticks
+ .replace(/\${/g, '\\${'); // Then escape template literal expressions
+
+ const outputContent = `// This file is auto-generated by scripts/bundle-adapter.js
+// Do not edit directly - modify adapter-runtime.ts instead
+
+export const ADAPTER_RUNTIME_SCRIPT = \`${escapedCode}\`;
+`;
+
+ writeFileSync(join(__dirname, '../src/adapters/appssdk/adapter-runtime.bundled.ts'), outputContent);
+ console.log('✅ Successfully bundled Apps SDK adapter runtime');
+} catch (error) {
+ console.error('❌ Failed to bundle adapter runtime:', error);
+ process.exit(1);
+}
diff --git a/sdks/typescript/server/src/__tests__/adapters/adapter-integration.test.ts b/sdks/typescript/server/src/__tests__/adapters/adapter-integration.test.ts
new file mode 100644
index 00000000..3e6854fb
--- /dev/null
+++ b/sdks/typescript/server/src/__tests__/adapters/adapter-integration.test.ts
@@ -0,0 +1,316 @@
+import { describe, it, expect } from 'vitest';
+import { createUIResource } from '../../index';
+import { wrapHtmlWithAdapters } from '../../utils';
+
+describe('Apps SDK Adapter Integration', () => {
+ describe('createUIResource with adapters', () => {
+ it('should create UI resource without adapter by default', () => {
+ const resource = createUIResource({
+ uri: 'ui://test',
+ content: {
+ type: 'rawHtml',
+ htmlString: 'Test
',
+ },
+ encoding: 'text',
+ });
+
+ expect(resource.resource.text).toBe('Test
');
+ expect(resource.resource.text).not.toContain('MCPUIAppsSdkAdapter');
+ });
+
+ it('should wrap HTML with Apps SDK adapter when enabled', () => {
+ const resource = createUIResource({
+ uri: 'ui://test',
+ content: {
+ type: 'rawHtml',
+ htmlString: 'Test
',
+ },
+ encoding: 'text',
+ adapters: {
+ appsSdk: {
+ enabled: true,
+ },
+ },
+ });
+
+ expect(resource.resource.text).toContain('');
+ expect(resource.resource.text).toContain('Test
');
+ });
+
+ it('should pass adapter config to the wrapper', () => {
+ const resource = createUIResource({
+ uri: 'ui://test',
+ content: {
+ type: 'rawHtml',
+ htmlString: 'Test
',
+ },
+ encoding: 'text',
+ adapters: {
+ appsSdk: {
+ enabled: true,
+ config: {
+ timeout: 5000,
+ intentHandling: 'ignore',
+ hostOrigin: 'https://custom.com',
+ },
+ },
+ },
+ });
+
+ const html = resource.resource.text as string;
+ expect(html).toContain('5000');
+ expect(html).toContain('ignore');
+ expect(html).toContain('https://custom.com');
+ });
+
+ it('should not wrap when adapter is disabled', () => {
+ const resource = createUIResource({
+ uri: 'ui://test',
+ content: {
+ type: 'rawHtml',
+ htmlString: 'Test
',
+ },
+ encoding: 'text',
+ adapters: {
+ appsSdk: {
+ enabled: false,
+ },
+ },
+ });
+
+ expect(resource.resource.text).toBe('Test
');
+ });
+
+ it('should work with HTML containing head tag', () => {
+ const resource = createUIResource({
+ uri: 'ui://test',
+ content: {
+ type: 'rawHtml',
+ htmlString: 'Test Content',
+ },
+ encoding: 'text',
+ adapters: {
+ appsSdk: {
+ enabled: true,
+ },
+ },
+ });
+
+ const html = resource.resource.text as string;
+ expect(html).toContain('');
+ expect(html).toContain('');
+ expect(wrapped).toContain(html);
+ });
+
+ it('should inject script in head tag if present', () => {
+ const html = 'Test
';
+ const wrapped = wrapHtmlWithAdapters(html, {
+ appsSdk: {
+ enabled: true,
+ },
+ });
+
+ const headIndex = wrapped.indexOf('');
+ const scriptIndex = wrapped.indexOf('');
+ expect(script.trim().startsWith('')).toBe(true);
+ });
+
+ it('should include the bundled adapter code', () => {
+ const script = getAppsSdkAdapterScript();
+
+ // Check for key adapter components
+ expect(script).toContain('MCPUIAppsSdkAdapter');
+ expect(script).toContain('initAdapter');
+ expect(script).toContain('uninstallAdapter');
+ });
+
+ it('should inject default config when no config provided', () => {
+ const script = getAppsSdkAdapterScript();
+
+ // Should call initAdapter with empty config
+ expect(script).toContain('initAdapter({})');
+ });
+
+ it('should inject custom timeout config', () => {
+ const config: AppsSdkAdapterConfig = {
+ timeout: 5000,
+ };
+
+ const script = getAppsSdkAdapterScript(config);
+
+ expect(script).toContain('5000');
+ expect(script).toContain('"timeout":5000');
+ });
+
+ it('should inject custom intentHandling config', () => {
+ const config: AppsSdkAdapterConfig = {
+ intentHandling: 'ignore',
+ };
+
+ const script = getAppsSdkAdapterScript(config);
+
+ expect(script).toContain('ignore');
+ expect(script).toContain('"intentHandling":"ignore"');
+ });
+
+ it('should inject custom hostOrigin config', () => {
+ const config: AppsSdkAdapterConfig = {
+ hostOrigin: 'https://custom.com',
+ };
+
+ const script = getAppsSdkAdapterScript(config);
+
+ expect(script).toContain('https://custom.com');
+ expect(script).toContain('"hostOrigin":"https://custom.com"');
+ });
+
+ it('should inject multiple config options', () => {
+ const config: AppsSdkAdapterConfig = {
+ timeout: 10000,
+ intentHandling: 'prompt',
+ hostOrigin: 'https://test.example.com',
+ };
+
+ const script = getAppsSdkAdapterScript(config);
+
+ expect(script).toContain('10000');
+ expect(script).toContain('prompt');
+ expect(script).toContain('https://test.example.com');
+ });
+
+ it('should set MCP_APPSSDK_ADAPTER_NO_AUTO_INSTALL flag', () => {
+ const script = getAppsSdkAdapterScript();
+
+ // Should prevent the bundled code from auto-initializing
+ expect(script).toContain('MCP_APPSSDK_ADAPTER_NO_AUTO_INSTALL');
+ expect(script).toContain('window.MCP_APPSSDK_ADAPTER_NO_AUTO_INSTALL = true');
+ });
+
+ it('should expose global MCPUIAppsSdkAdapter API', () => {
+ const script = getAppsSdkAdapterScript();
+
+ expect(script).toContain('window.MCPUIAppsSdkAdapter');
+ expect(script).toContain('init: initAdapter');
+ expect(script).toContain('uninstall: uninstallAdapter');
+ });
+
+ it('should check for window before initialization', () => {
+ const script = getAppsSdkAdapterScript();
+
+ // Should have window checks
+ expect(script).toContain("typeof window !== 'undefined'");
+ });
+
+ it('should be wrapped in IIFE', () => {
+ const script = getAppsSdkAdapterScript();
+
+ // Should be wrapped in a function to avoid global pollution
+ expect(script).toContain('(function()');
+ expect(script).toContain('})()');
+ });
+
+ it('should include use strict directive', () => {
+ const script = getAppsSdkAdapterScript();
+
+ expect(script).toContain("'use strict'");
+ });
+
+ it('should contain core adapter functionality keywords', () => {
+ const script = getAppsSdkAdapterScript();
+
+ // Check for essential adapter components
+ expect(script).toContain('postMessage');
+ expect(script).toContain('window.openai');
+ expect(script).toContain('handleMCPUIMessage');
+ });
+
+ it('should support tool calling', () => {
+ const script = getAppsSdkAdapterScript();
+
+ expect(script).toContain('callTool');
+ expect(script).toContain('tool');
+ });
+
+ it('should support prompt sending', () => {
+ const script = getAppsSdkAdapterScript();
+
+ expect(script).toContain('sendFollowUpMessage');
+ expect(script).toContain('prompt');
+ });
+
+ it('should handle widget state', () => {
+ const script = getAppsSdkAdapterScript();
+
+ expect(script).toContain('widgetState');
+ // widgetState is read from Apps SDK
+ });
+
+ it('should handle render data', () => {
+ const script = getAppsSdkAdapterScript();
+
+ expect(script).toContain('renderData');
+ expect(script).toContain('toolInput');
+ expect(script).toContain('toolOutput');
+ });
+
+ it('should handle lifecycle messages', () => {
+ const script = getAppsSdkAdapterScript();
+
+ expect(script).toContain('ui-lifecycle');
+ expect(script).toContain('ui-request');
+ });
+
+ it('should properly escape JSON in config', () => {
+ const config: AppsSdkAdapterConfig = {
+ hostOrigin: 'https://example.com/"test"',
+ };
+
+ // Should not throw and should properly escape quotes
+ expect(() => getAppsSdkAdapterScript(config)).not.toThrow();
+
+ const script = getAppsSdkAdapterScript(config);
+ // The JSON.stringify should handle escaping
+ expect(script).toContain('\\"test\\"');
+ });
+ });
+
+ describe('Type Definitions', () => {
+ it('should accept valid config types', () => {
+ const validConfigs: AppsSdkAdapterConfig[] = [
+ {},
+ { timeout: 5000 },
+ { intentHandling: 'prompt' },
+ { intentHandling: 'ignore' },
+ { hostOrigin: 'https://example.com' },
+ {
+ timeout: 10000,
+ intentHandling: 'ignore',
+ hostOrigin: 'https://test.com',
+ },
+ ];
+
+ for (const config of validConfigs) {
+ expect(() => getAppsSdkAdapterScript(config)).not.toThrow();
+ }
+ });
+ });
+
+ describe('Script Size', () => {
+ it('should generate a reasonably sized script', () => {
+ const script = getAppsSdkAdapterScript();
+
+ // Script should be present but not excessively large
+ expect(script.length).toBeGreaterThan(100);
+ expect(script.length).toBeLessThan(50000); // ~50KB max
+ });
+
+ it('should not significantly grow with config', () => {
+ const baseScript = getAppsSdkAdapterScript();
+ const configuredScript = getAppsSdkAdapterScript({
+ timeout: 10000,
+ intentHandling: 'ignore',
+ hostOrigin: 'https://example.com',
+ });
+
+ // Config should only add a small amount to script size
+ const sizeDiff = configuredScript.length - baseScript.length;
+ expect(sizeDiff).toBeLessThan(200);
+ });
+ });
+
+ describe('Script Validity', () => {
+ it('should generate syntactically valid JavaScript', () => {
+ const script = getAppsSdkAdapterScript();
+
+ // Extract just the JavaScript code (remove
+`.trim();
+}
diff --git a/sdks/typescript/server/src/adapters/appssdk/index.ts b/sdks/typescript/server/src/adapters/appssdk/index.ts
new file mode 100644
index 00000000..0eba3d29
--- /dev/null
+++ b/sdks/typescript/server/src/adapters/appssdk/index.ts
@@ -0,0 +1,14 @@
+/**
+ * Apps SDK Adapter for MCP-UI
+ *
+ * Enables MCP-UI widgets to work in Apps SDK environments (e.g., ChatGPT)
+ */
+
+export { getAppsSdkAdapterScript } from './adapter.js';
+export type {
+ AppsSdkAdapterConfig,
+ AppsSdkBridge,
+ RenderData,
+ MCPUIMessage,
+} from './types.js';
+
diff --git a/sdks/typescript/server/src/adapters/appssdk/types.ts b/sdks/typescript/server/src/adapters/appssdk/types.ts
new file mode 100644
index 00000000..5a8244fd
--- /dev/null
+++ b/sdks/typescript/server/src/adapters/appssdk/types.ts
@@ -0,0 +1,171 @@
+/**
+ * Type definitions for the Apps SDK adapter
+ */
+
+import type { UIActionResult } from '../../types.js';
+
+/**
+ * Additional MCP-UI Protocol Messages (Lifecycle & Control)
+ */
+
+export interface MCPUILifecycleReadyMessage {
+ type: 'ui-lifecycle-iframe-ready';
+ messageId?: string;
+ payload?: Record;
+}
+
+export interface MCPUISizeChangeMessage {
+ type: 'ui-size-change';
+ messageId?: string;
+ payload: {
+ width?: number;
+ height?: number;
+ };
+}
+
+export interface MCPUIRequestDataMessage {
+ type: 'ui-request-data';
+ messageId: string;
+ payload: {
+ requestType: string;
+ params?: Record;
+ };
+}
+
+export interface MCPUIRequestRenderDataMessage {
+ type: 'ui-request-render-data';
+ messageId?: string;
+ payload?: Record;
+}
+
+export interface MCPUIRenderDataMessage {
+ type: 'ui-lifecycle-iframe-render-data';
+ messageId?: string;
+ payload: {
+ renderData: RenderData;
+ };
+}
+
+export interface MCPUIMessageReceivedMessage {
+ type: 'ui-message-received';
+ messageId?: string;
+ payload: {
+ messageId: string;
+ };
+}
+
+export interface MCPUIMessageResponseMessage {
+ type: 'ui-message-response';
+ messageId?: string;
+ payload: {
+ messageId: string;
+ response?: unknown;
+ error?: unknown;
+ };
+}
+
+export interface RenderData {
+ toolInput?: Record;
+ toolOutput?: unknown;
+ widgetState?: unknown;
+ locale?: string;
+ theme?: string;
+ displayMode?: 'inline' | 'pip' | 'fullscreen';
+ maxHeight?: number;
+ [key: string]: unknown;
+}
+
+export type MCPUIMessage =
+ | UIActionResult
+ | MCPUILifecycleReadyMessage
+ | MCPUISizeChangeMessage
+ | MCPUIRequestDataMessage
+ | MCPUIRequestRenderDataMessage
+ | MCPUIRenderDataMessage
+ | MCPUIMessageReceivedMessage
+ | MCPUIMessageResponseMessage;
+
+/**
+ * Apps SDK Protocol Types (e.g., ChatGPT window.openai interface)
+ */
+
+export interface AppsSdkBridge {
+ toolInput: Record;
+ toolOutput: unknown;
+ widgetState: unknown;
+ setWidgetState(state: unknown): Promise;
+ callTool(name: string, args: Record): Promise;
+ sendFollowUpMessage(params: { prompt: string }): Promise;
+ requestDisplayMode(params: { mode: 'inline' | 'pip' | 'fullscreen' }): Promise;
+ maxHeight?: number;
+ displayMode?: 'inline' | 'pip' | 'fullscreen';
+ locale?: string;
+ theme?: string;
+}
+
+export interface AppsSdkSetGlobalsEventDetail {
+ displayMode?: 'inline' | 'pip' | 'fullscreen';
+ maxHeight?: number;
+ toolOutput?: unknown;
+ widgetState?: unknown;
+ locale?: string;
+ theme?: string;
+}
+
+export interface AppsSdkToolResponseEventDetail {
+ name: string;
+ args: Record;
+ result: unknown;
+}
+
+/**
+ * Apps SDK Adapter Configuration
+ */
+
+export interface AppsSdkAdapterConfig {
+ /**
+ * Custom logger (defaults to console)
+ */
+ logger?: Pick;
+
+ /**
+ * Origin to use when dispatching MessageEvents to the iframe (defaults to window.location.origin)
+ * This simulates the origin of the host/parent window
+ */
+ hostOrigin?: string;
+
+ /**
+ * Timeout in milliseconds for async operations (defaults to 30000)
+ */
+ timeout?: number;
+
+ /**
+ * How to handle 'intent' messages (defaults to 'prompt')
+ * - 'prompt': Convert to sendFollowupTurn with intent description
+ * - 'ignore': Log and acknowledge but take no action
+ */
+ intentHandling?: 'prompt' | 'ignore';
+}
+
+/**
+ * Internal adapter state
+ */
+
+export interface PendingRequest {
+ messageId: string;
+ type: string;
+ resolve: (value: T | PromiseLike) => void;
+ reject: (error: unknown) => void;
+ timeoutId: ReturnType;
+}
+
+/**
+ * Window extension for Apps SDK environment (e.g., window.openai in ChatGPT)
+ */
+
+declare global {
+ interface Window {
+ openai?: AppsSdkBridge;
+ }
+}
+
diff --git a/sdks/typescript/server/src/adapters/index.ts b/sdks/typescript/server/src/adapters/index.ts
new file mode 100644
index 00000000..647aa7f3
--- /dev/null
+++ b/sdks/typescript/server/src/adapters/index.ts
@@ -0,0 +1,14 @@
+/**
+ * MCP-UI Adapters
+ *
+ * Adapters enable MCP-UI widgets to work in host-specific environments by translating
+ * the open MCP-UI protocol to platform-specific APIs.
+ *
+ * Available adapters:
+ * - appsSdk: For Apps SDK environments (e.g., ChatGPT)
+ *
+ * Future adapters could include other platforms as needed.
+ */
+
+export * from './appssdk/index.js';
+
diff --git a/sdks/typescript/server/src/index.ts b/sdks/typescript/server/src/index.ts
index b567279d..f554936f 100644
--- a/sdks/typescript/server/src/index.ts
+++ b/sdks/typescript/server/src/index.ts
@@ -10,7 +10,7 @@ import {
UIActionResultIntent,
UIActionResultToolCall,
} from './types.js';
-import { getAdditionalResourceProps, utf8ToBase64 } from './utils.js';
+import { getAdditionalResourceProps, utf8ToBase64, wrapHtmlWithAdapters } from './utils.js';
export type UIResource = {
type: 'resource';
@@ -39,6 +39,12 @@ export function createUIResource(options: CreateUIResourceOptions): UIResource {
"MCP-UI SDK: content.htmlString must be provided as a string when content.type is 'rawHtml'.",
);
}
+
+ // Wrap with adapters if any are enabled
+ if (options.adapters) {
+ actualContentString = wrapHtmlWithAdapters(actualContentString, options.adapters);
+ }
+
mimeType = 'text/html';
} else if (options.content.type === 'externalUrl') {
if (!options.uri.startsWith('ui://')) {
@@ -102,7 +108,17 @@ export function createUIResource(options: CreateUIResourceOptions): UIResource {
};
}
-export type { CreateUIResourceOptions, ResourceContentPayload, UIActionResult } from './types.js';
+export type {
+ CreateUIResourceOptions,
+ ResourceContentPayload,
+ UIActionResult,
+ AdaptersConfig,
+ AppsSdkAdapterOptions,
+} from './types.js';
+
+// Export adapters
+export { wrapHtmlWithAdapters } from './utils.js';
+export * from './adapters/index.js';
export function postUIActionResult(result: UIActionResult): void {
if (window.parent) {
diff --git a/sdks/typescript/server/src/types.ts b/sdks/typescript/server/src/types.ts
index db875328..4f96c68f 100644
--- a/sdks/typescript/server/src/types.ts
+++ b/sdks/typescript/server/src/types.ts
@@ -47,6 +47,58 @@ export interface CreateUIResourceOptions {
resourceProps?: UIResourceProps;
// additional resource props to be passed on the top-level embedded resource (i.e. annotations)
embeddedResourceProps?: EmbeddedUIResourceProps;
+ // Adapters for different environments (e.g., Apps SDK)
+ adapters?: AdaptersConfig;
+}
+
+/**
+ * Configuration for all available adapters
+ * Adapters enable MCP-UI widgets to work in different environments
+ */
+export interface AdaptersConfig {
+ /**
+ * Apps SDK adapter (e.g., ChatGPT)
+ * Translates MCP-UI protocol to Apps SDK API calls (window.openai)
+ */
+ appsSdk?: AppsSdkAdapterOptions;
+
+ // Future adapters can be added here
+ // e.g., anotherPlatform?: AnotherPlatformAdapterOptions;
+}
+
+/**
+ * Configuration options for Apps SDK adapter
+ */
+export interface AppsSdkAdapterOptions {
+ /**
+ * Whether to enable the Apps SDK adapter.
+ * When enabled, the adapter script will be automatically injected into HTML content,
+ * allowing MCP-UI widgets to work in Apps SDK environments (e.g., ChatGPT).
+ * @default false
+ */
+ enabled: boolean;
+
+ /**
+ * Custom configuration for the adapter
+ */
+ config?: {
+ /**
+ * How to handle 'intent' messages (defaults to 'prompt')
+ * - 'prompt': Convert to sendFollowupTurn with intent description
+ * - 'ignore': Log and acknowledge but take no action
+ */
+ intentHandling?: 'prompt' | 'ignore';
+
+ /**
+ * Timeout in milliseconds for async operations (defaults to 30000)
+ */
+ timeout?: number;
+
+ /**
+ * Origin to use when dispatching MessageEvents to the iframe (defaults to window.location.origin)
+ */
+ hostOrigin?: string;
+ };
}
export type UIResourceProps = Omit, 'uri' | 'mimeType'>;
diff --git a/sdks/typescript/server/src/utils.ts b/sdks/typescript/server/src/utils.ts
index ee80237a..d3178b1f 100644
--- a/sdks/typescript/server/src/utils.ts
+++ b/sdks/typescript/server/src/utils.ts
@@ -1,5 +1,6 @@
-import type { CreateUIResourceOptions, UIResourceProps } from './types.js';
+import type { CreateUIResourceOptions, UIResourceProps, AdaptersConfig } from './types.js';
import { UI_METADATA_PREFIX } from './types.js';
+import { getAppsSdkAdapterScript } from './adapters/appssdk/adapter.js';
export function getAdditionalResourceProps(
resourceOptions: Partial,
@@ -60,3 +61,52 @@ export function utf8ToBase64(str: string): string {
}
}
}
+
+/**
+ * Wraps HTML content with enabled adapter scripts.
+ * This allows the HTML to communicate with different platform environments.
+ *
+ * @param htmlContent - The HTML content to wrap
+ * @param adaptersConfig - Configuration for all adapters
+ * @returns The wrapped HTML content with adapter scripts injected
+ */
+export function wrapHtmlWithAdapters(
+ htmlContent: string,
+ adaptersConfig?: AdaptersConfig,
+): string {
+ if (!adaptersConfig) {
+ return htmlContent;
+ }
+
+ const wrappedHtml = htmlContent;
+ const adapterScripts: string[] = [];
+
+ // Apps SDK adapter
+ if (adaptersConfig.appsSdk?.enabled) {
+ const script = getAppsSdkAdapterScript(adaptersConfig.appsSdk.config);
+ adapterScripts.push(script);
+ }
+
+ // Future adapters can be added here by checking for their config and pushing their scripts to adapterScripts.
+
+ // If no adapters are enabled, return original HTML
+ if (adapterScripts.length === 0) {
+ return htmlContent;
+ }
+
+ // Combine all adapter scripts
+ const combinedScripts = adapterScripts.join('\n');
+
+ // If the HTML already has a tag, inject the adapter scripts into it
+ if (wrappedHtml.includes('')) {
+ return wrappedHtml.replace('', `\n${combinedScripts}`);
+ }
+
+ // If the HTML has an tag but no , add a with the adapter scripts
+ if (wrappedHtml.includes('')) {
+ return wrappedHtml.replace('', `\n\n${combinedScripts}\n`);
+ }
+
+ // Otherwise, prepend the adapter scripts before the content
+ return `${combinedScripts}\n${wrappedHtml}`;
+}