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

+ +
+ + + + + ` + }, + 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

+ +
+ + + + + + + ` + }, + 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

+ +
+
+ + + + + ` + }, + 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

+ + + +
+ + + +
+ +
+
+ + + + + ` + }, + 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: ` + + ` + }, + 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( + '', + { + 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: 'TestContent', + }, + 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}`; +}