diff --git a/app/app.css b/app/app.css
index c1b15a9..1f0a692 100644
--- a/app/app.css
+++ b/app/app.css
@@ -1,88 +1,83 @@
+/**
+ * Design System Styles
+ *
+ * These styles follow the OpenAI Apps SDK design guidelines:
+ * https://developers.openai.com/apps-sdk/concepts/design-guidelines
+ *
+ * Key principles:
+ * - Platform-native system fonts (SF Pro, Roboto)
+ * - Limited font size variation (body and body-small preferred)
+ * - System colors for text, icons, and spatial elements
+ * - DO NOT override backgrounds or text colors (let system control these)
+ * - Brand colors ONLY on primary buttons/CTAs
+ * - WCAG AA contrast ratios for accessibility
+ * - Consistent spacing and corner radius
+ */
+
@import 'tailwindcss' source('.');
@theme {
--font-sans:
- 'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
+ -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Display',
+ 'Roboto', system-ui, ui-sans-serif, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
html,
body {
- @media (prefers-color-scheme: dark) {
- color-scheme: dark;
- }
+ /* Support both light and dark color schemes to match parent frame */
+ color-scheme: light dark;
+ /* Transparent background to inherit from parent frame when embedded */
+ background: transparent !important;
}
:root {
- --radius: 0.5rem;
+ /* System-consistent corner radius per OpenAI Apps SDK guidelines */
+ --radius: 0.75rem;
- --background: oklch(98% 0.02 320);
- --foreground: oklch(20% 0.06 320);
+ /* Inherit system background and foreground - don't override */
+ /* Per guidelines: partners should not override backgrounds or text colors */
+ --background: transparent;
+ --foreground: light-dark(oklch(0% 0 0), oklch(100% 0 0));
- --card: oklch(99% 0.015 320);
- --card-foreground: oklch(20% 0.06 320);
+ /* Minimal card surface - barely distinguishable from background */
+ --card: light-dark(oklch(98% 0 0), oklch(10% 0 0));
+ --card-foreground: light-dark(oklch(0% 0 0), oklch(100% 0 0));
- --popover: oklch(99% 0.015 320);
- --popover-foreground: oklch(20% 0.06 320);
+ /* Popover surfaces */
+ --popover: light-dark(oklch(100% 0 0), oklch(10% 0 0));
+ --popover-foreground: light-dark(oklch(0% 0 0), oklch(100% 0 0));
+ /* Brand accent for primary actions (buttons, CTAs) - only place for brand color */
--primary: oklch(65% 0.15 320);
- --primary-foreground: oklch(98% 0.02 320);
+ --primary-foreground: oklch(100% 0 0);
- --secondary: oklch(85% 0.08 330);
- --secondary-foreground: oklch(20% 0.06 320);
+ /* Secondary UI elements - neutral grays */
+ --secondary: light-dark(oklch(95% 0 0), oklch(20% 0 0));
+ --secondary-foreground: light-dark(oklch(0% 0 0), oklch(100% 0 0));
- --muted: oklch(92% 0.04 320);
- --muted-foreground: oklch(40% 0.07 320);
+ /* Muted elements - subtle text */
+ --muted: light-dark(oklch(96% 0 0), oklch(15% 0 0));
+ --muted-foreground: light-dark(oklch(45% 0 0), oklch(65% 0 0));
- --accent: oklch(88% 0.06 340);
- --accent-foreground: oklch(20% 0.06 320);
+ /* Accent elements (badges, highlights) - use sparingly */
+ --accent: light-dark(oklch(96% 0 0), oklch(15% 0 0));
+ --accent-foreground: light-dark(oklch(0% 0 0), oklch(100% 0 0));
- --border: oklch(88% 0.05 320);
- --input: oklch(88% 0.05 320);
- --input-invalid: oklch(65% 0.2 0);
+ /* Borders and dividers - spatial elements only */
+ --border: light-dark(oklch(85% 0 0), oklch(30% 0 0));
+ --input: light-dark(oklch(85% 0 0), oklch(30% 0 0));
+ --input-invalid: oklch(60% 0.2 20);
- --destructive: oklch(60% 0.18 0);
- --destructive-foreground: oklch(98% 0.02 320);
- --foreground-destructive: oklch(55% 0.18 0);
+ /* Destructive actions */
+ --destructive: oklch(55% 0.22 25);
+ --destructive-foreground: oklch(100% 0 0);
+ --foreground-destructive: oklch(50% 0.22 25);
+ /* Focus ring - use brand accent */
--ring: oklch(65% 0.15 320);
}
-@media (prefers-color-scheme: dark) {
- :root {
- --background: oklch(15% 0.05 320);
- --foreground: oklch(95% 0.02 320);
-
- --card: oklch(18% 0.05 320);
- --card-foreground: oklch(95% 0.02 320);
-
- --popover: oklch(18% 0.05 320);
- --popover-foreground: oklch(95% 0.02 320);
-
- --primary: oklch(70% 0.18 320);
- --primary-foreground: oklch(15% 0.05 320);
-
- --secondary: oklch(30% 0.08 330);
- --secondary-foreground: oklch(95% 0.02 320);
-
- --muted: oklch(25% 0.06 320);
- --muted-foreground: oklch(65% 0.05 320);
-
- --accent: oklch(28% 0.06 340);
- --accent-foreground: oklch(95% 0.02 320);
-
- --border: oklch(28% 0.06 320);
- --input: oklch(28% 0.06 320);
- --input-invalid: oklch(50% 0.18 0);
-
- --destructive: oklch(55% 0.18 0);
- --destructive-foreground: oklch(95% 0.02 320);
- --foreground-destructive: oklch(65% 0.2 0);
-
- --ring: oklch(70% 0.18 320);
- }
-}
-
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
@@ -112,56 +107,43 @@ body {
}
@theme {
- --text-mega: 5rem;
- --text-mega--line-height: 5.25rem;
- --text-mega--font-weight: 700;
- --text-h1: 3.5rem;
- --text-h1--line-height: 3.875rem;
- --text-h1--font-weight: 700;
- --text-h2: 2.5rem;
- --text-h2--line-height: 3rem;
- --text-h2--font-weight: 700;
- --text-h3: 2rem;
- --text-h3--line-height: 2.25rem;
- --text-h3--font-weight: 700;
- --text-h4: 1.75rem;
- --text-h4--line-height: 2.25rem;
- --text-h4--font-weight: 700;
- --text-h5: 1.5rem;
- --text-h5--line-height: 2rem;
- --text-h5--font-weight: 700;
- --text-h6: 1rem;
- --text-h6--line-height: 1.25rem;
- --text-h6--font-weight: 700;
- --text-body-2xl: 2rem;
- --text-body-2xl--line-height: 2.25rem;
- --text-body-xl: 1.75rem;
- --text-body-xl--line-height: 2.25rem;
- --text-body-lg: 1.5rem;
- --text-body-lg--line-height: 2rem;
- --text-body-md: 1.25rem;
- --text-body-md--line-height: 1.75rem;
- --text-body-sm: 1rem;
+ /* Simplified typography scale per OpenAI Apps SDK guidelines */
+ /* Limit variation in font size, preferring body and body-small sizes */
+ --text-h1: 1.5rem;
+ --text-h1--line-height: 2rem;
+ --text-h1--font-weight: 600;
+ --text-h2: 1.25rem;
+ --text-h2--line-height: 1.75rem;
+ --text-h2--font-weight: 600;
+ --text-h3: 1.125rem;
+ --text-h3--line-height: 1.5rem;
+ --text-h3--font-weight: 600;
+ --text-body: 1rem;
+ --text-body--line-height: 1.5rem;
+ --text-body--font-weight: 400;
+ --text-body-sm: 0.875rem;
--text-body-sm--line-height: 1.25rem;
- --text-body-xs: 0.875rem;
- --text-body-xs--line-height: 1.125rem;
- --text-body-2xs: 0.75rem;
- --text-body-2xs--line-height: 1rem;
- --text-caption: 1.125rem;
- --text-caption--line-height: 1.5rem;
- --text-caption--font-weight: 600;
- --text-button: 0.75rem;
- --text-button--line-height: 1rem;
- --text-button--font-weight: 700;
+ --text-body-sm--font-weight: 400;
+ --text-caption: 0.75rem;
+ --text-caption--line-height: 1rem;
+ --text-caption--font-weight: 500;
}
@utility container {
+ /* System grid spacing per OpenAI Apps SDK guidelines */
margin-inline: auto;
- padding-inline: 2rem;
+ padding-inline: 1rem;
+
+ @media (width >= 768px) {
+ & {
+ padding-inline: 1.5rem;
+ }
+ }
- @media (width >=1400px) {
+ @media (width >= 1400px) {
& {
max-width: 1400px;
+ padding-inline: 2rem;
}
}
}
diff --git a/app/root.tsx b/app/root.tsx
index 68fdea7..267b5eb 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -16,6 +16,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
+
@@ -47,6 +48,7 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
details = error.message
stack = error.stack
}
+ console.error(error)
return (
diff --git a/app/routes.ts b/app/routes.ts
index 1692343..7d9680b 100644
--- a/app/routes.ts
+++ b/app/routes.ts
@@ -2,6 +2,9 @@ import { type RouteConfig, index, route } from '@react-router/dev/routes'
export default [
index('routes/index.tsx'),
+ // chat gpt hosts your app on their own server and serves it at /index.html
+ route('index.html', 'routes/chat-gpt-app/index.tsx'),
+
route('authorize', 'routes/authorize.tsx'),
route('ui/token-input', 'routes/ui/token-input.tsx'),
route('ui/journal-viewer', 'routes/ui/journal-viewer.tsx'),
diff --git a/app/routes/chat-gpt-app/index.tsx b/app/routes/chat-gpt-app/index.tsx
new file mode 100644
index 0000000..2dd8004
--- /dev/null
+++ b/app/routes/chat-gpt-app/index.tsx
@@ -0,0 +1,259 @@
+import { useState, useTransition } from 'react'
+import {
+ ErrorBoundary,
+ useErrorBoundary,
+ type FallbackProps,
+} from 'react-error-boundary'
+import { z } from 'zod'
+import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
+import {
+ useMcpUiInit,
+ sendMcpMessage,
+ waitForRenderData,
+} from '#app/utils/mcp.ts'
+import { useDoubleCheck, useUnmountSignal } from '#app/utils/misc.ts'
+import { type Route } from './+types/index.tsx'
+
+export async function clientLoader() {
+ const renderDataSchema = z
+ .object({
+ toolOutput: z.object({
+ entry: z.object({
+ id: z.number(),
+ title: z.string(),
+ content: z.string(),
+ tags: z.array(z.object({ id: z.number(), name: z.string() })),
+ mood: z.string().nullable().optional(),
+ location: z.string().nullable().optional(),
+ weather: z.string().nullable().optional(),
+ createdAt: z.number(),
+ updatedAt: z.number(),
+ }),
+ }),
+ })
+ .passthrough()
+ const renderData = await waitForRenderData(renderDataSchema)
+ console.log({ renderData })
+ return { entry: renderData.toolOutput.entry }
+}
+
+export function HydrateFallback() {
+ return (
+
+
+
+ Waiting for journal entries...
+
+
+ )
+}
+
+export default function EntryViewerContent({
+ loaderData,
+}: Route.ComponentProps) {
+ const { entry } = loaderData
+ const [isDeleted, setIsDeleted] = useState(false)
+
+ useMcpUiInit()
+
+ if (isDeleted) {
+ return (
+
+
+
+
+ Entry Deleted
+
+
+ Entry deleted successfully
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+ {entry.title}
+
+
+
+
+ {entry.tags.length > 0 ? (
+ entry.tags.map((tag) => (
+
+ 🏷️ {tag.name}
+
+ ))
+ ) : (
+ No tags
+ )}
+
+
+
+ {entry.mood && (
+
+ 💭
+
+ {entry.mood}
+
+
+ )}
+ {entry.location && (
+
+ 📍
+
+ {entry.location}
+
+
+ )}
+ {entry.weather && (
+
+ 🌤️
+
+ {entry.weather}
+
+
+ )}
+
+ 📅
+
+ Created: {new Date(entry.createdAt * 1000).toLocaleDateString()}
+
+
+ {entry.updatedAt !== entry.createdAt && (
+
+ ✏️
+
+ Updated:{' '}
+ {new Date(entry.updatedAt * 1000).toLocaleDateString()}
+
+
+ )}
+
+
+
+
+
+ Content
+
+
+ {entry.content}
+
+
+
+
+ setIsDeleted(true)}
+ />
+
+
+
+ )
+}
+
+function DeleteEntryButton({
+ entry,
+ onDeleted,
+}: {
+ entry: { id: number; title: string }
+ onDeleted: () => void
+}) {
+ return (
+
+
+
+ )
+}
+
+function DeleteEntryError({ error, resetErrorBoundary }: FallbackProps) {
+ return (
+
+
Failed to delete entry
+
{error.message}
+
+
+ )
+}
+
+function DeleteEntryButtonImpl({
+ entry,
+ onDeleted,
+}: {
+ entry: { id: number; title: string }
+ onDeleted: () => void
+}) {
+ const [isPending, startTransition] = useTransition()
+ const { doubleCheck, getButtonProps } = useDoubleCheck()
+ const { showBoundary } = useErrorBoundary()
+ const unmountSignal = useUnmountSignal()
+
+ const handleDelete = async () => {
+ if (!doubleCheck) return
+
+ startTransition(async () => {
+ try {
+ await sendMcpMessage(
+ 'tool',
+ { toolName: 'delete_entry', params: { id: entry.id } },
+ { signal: unmountSignal },
+ )
+ onDeleted()
+ } catch (err) {
+ showBoundary(err)
+ }
+ })
+ }
+
+ return (
+
+ )
+}
+
+export { GeneralErrorBoundary as ErrorBoundary }
diff --git a/app/routes/ui/journal-viewer.tsx b/app/routes/ui/journal-viewer.tsx
index 57305bd..17534fa 100644
--- a/app/routes/ui/journal-viewer.tsx
+++ b/app/routes/ui/journal-viewer.tsx
@@ -13,7 +13,7 @@ import {
import { useDoubleCheck, useUnmountSignal } from '#app/utils/misc.ts'
import { type Route } from './+types/journal-viewer.tsx'
-export async function clientLoader({ request }: Route.ClientLoaderArgs) {
+export async function clientLoader() {
const renderData = await waitForRenderData(
z.object({
entries: z.array(
@@ -24,7 +24,6 @@ export async function clientLoader({ request }: Route.ClientLoaderArgs) {
}),
),
}),
- { signal: request.signal, timeoutMs: 3_000 },
)
return { entries: renderData.entries }
}
diff --git a/app/utils/mcp-ui-compat.client.ts b/app/utils/mcp-ui-compat.client.ts
new file mode 100644
index 0000000..49d0c5c
--- /dev/null
+++ b/app/utils/mcp-ui-compat.client.ts
@@ -0,0 +1,434 @@
+// thanks https://gist.github.com/liady/aa3a4095f39f859a5877d1bcd710476c
+/* eslint-disable */
+// @ts-nocheck
+
+var __defProp = Object.defineProperty
+var __defNormalProp = (obj, key, value) =>
+ key in obj
+ ? __defProp(obj, key, {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value,
+ })
+ : (obj[key] = value)
+var __publicField = (obj, key, value) => {
+ __defNormalProp(obj, typeof key !== 'symbol' ? key + '' : key, value)
+ return value
+}
+// src/adapters/appssdk/adapter-runtime.ts
+var MCPUIAppsSdkAdapter = class {
+ constructor(config = {}) {
+ __publicField(this, 'config')
+ __publicField(this, 'pendingRequests', /* @__PURE__ */ new Map())
+ __publicField(this, 'messageIdCounter', 0)
+ __publicField(this, 'originalPostMessage', null)
+ this.config = {
+ logger: config.logger || console,
+ hostOrigin: config.hostOrigin || window.location.origin,
+ timeout: config.timeout || 3e4,
+ intentHandling: config.intentHandling || 'prompt',
+ }
+ }
+ /**
+ * Initialize the adapter and monkey-patch postMessage if Apps SDK is present
+ */
+ install() {
+ if (!window.openai) {
+ this.config.logger.warn(
+ '[MCPUI-Apps SDK Adapter] window.openai not detected. Adapter will not activate.',
+ )
+ return false
+ }
+ this.config.logger.log('[MCPUI-Apps SDK Adapter] Initializing adapter...')
+ this.patchPostMessage()
+ this.setupAppsSdkEventListeners()
+ this.sendRenderData()
+ this.config.logger.log(
+ '[MCPUI-Apps SDK Adapter] Adapter initialized successfully',
+ )
+ return true
+ }
+ /**
+ * Clean up pending requests and restore original postMessage
+ */
+ uninstall() {
+ for (const request of this.pendingRequests.values()) {
+ clearTimeout(request.timeoutId)
+ request.reject(new Error('Adapter uninstalled'))
+ }
+ this.pendingRequests.clear()
+ if (this.originalPostMessage) {
+ try {
+ window.parent.postMessage = this.originalPostMessage
+ this.config.logger.log(
+ '[MCPUI-Apps SDK Adapter] Restored original parent.postMessage',
+ )
+ } catch (error) {
+ this.config.logger.error(
+ '[MCPUI-Apps SDK Adapter] Failed to restore original postMessage:',
+ error,
+ )
+ }
+ }
+ this.config.logger.log('[MCPUI-Apps SDK Adapter] Adapter uninstalled')
+ }
+ /**
+ * Monkey-patch parent.postMessage to intercept MCP-UI messages
+ * and forward non-MCP-UI messages to the original postMessage
+ */
+ patchPostMessage() {
+ this.originalPostMessage =
+ window.parent?.postMessage?.bind(window.parent) || null
+ if (!this.originalPostMessage) {
+ this.config.logger.debug(
+ '[MCPUI-Apps SDK Adapter] parent.postMessage does not exist, installing shim only',
+ )
+ } else {
+ this.config.logger.debug(
+ '[MCPUI-Apps SDK Adapter] Monkey-patching parent.postMessage to intercept MCP-UI messages',
+ )
+ }
+ const postMessageInterceptor = (message, targetOrigin, transfer) => {
+ if (this.isMCPUIMessage(message)) {
+ this.config.logger.debug(
+ '[MCPUI-Apps SDK Adapter] Intercepted MCP-UI message:',
+ message.type,
+ )
+ this.handleMCPUIMessage(message)
+ } else {
+ if (this.originalPostMessage) {
+ this.config.logger.debug(
+ '[MCPUI-Apps SDK Adapter] Forwarding non-MCP-UI message to original postMessage',
+ )
+ this.originalPostMessage(message, targetOrigin, transfer)
+ } else {
+ this.config.logger.warn(
+ '[MCPUI-Apps SDK Adapter] No original postMessage to forward to, ignoring message:',
+ message,
+ )
+ }
+ }
+ }
+ try {
+ window.parent.postMessage = postMessageInterceptor
+ } catch (error) {
+ this.config.logger.error(
+ '[MCPUI-Apps SDK Adapter] Failed to monkey-patch parent.postMessage:',
+ error,
+ )
+ }
+ }
+ /**
+ * Check if a message is an MCP-UI protocol message
+ */
+ isMCPUIMessage(message) {
+ if (!message || typeof message !== 'object') {
+ return false
+ }
+ const msg = message
+ return (
+ typeof msg.type === 'string' &&
+ (msg.type.startsWith('ui-') ||
+ ['tool', 'prompt', 'intent', 'notify', 'link'].includes(msg.type))
+ )
+ }
+ /**
+ * Handle incoming MCP-UI messages and translate to Apps SDK actions
+ */
+ async handleMCPUIMessage(message) {
+ this.config.logger.debug(
+ '[MCPUI-Apps SDK Adapter] Received MCPUI message:',
+ message.type,
+ )
+ try {
+ switch (message.type) {
+ case 'tool':
+ await this.handleToolMessage(message)
+ break
+ case 'prompt':
+ await this.handlePromptMessage(message)
+ break
+ case 'intent':
+ await this.handleIntentMessage(message)
+ break
+ case 'notify':
+ await this.handleNotifyMessage(message)
+ break
+ case 'link':
+ await this.handleLinkMessage(message)
+ break
+ case 'ui-lifecycle-iframe-ready':
+ this.sendRenderData()
+ break
+ case 'ui-request-render-data':
+ this.sendRenderData(message.messageId)
+ break
+ case 'ui-size-change':
+ this.handleSizeChange(message)
+ break
+ case 'ui-request-data':
+ this.handleRequestData(message)
+ break
+ default:
+ this.config.logger.warn(
+ '[MCPUI-Apps SDK Adapter] Unknown message type:',
+ message.type,
+ )
+ }
+ } catch (error) {
+ this.config.logger.error(
+ '[MCPUI-Apps SDK Adapter] Error handling message:',
+ error,
+ )
+ if (message.messageId) {
+ this.sendErrorResponse(message.messageId, error)
+ }
+ }
+ }
+ /**
+ * Handle 'tool' message - call Apps SDK tool
+ */
+ async handleToolMessage(message) {
+ if (message.type !== 'tool') return
+ const { toolName, params } = message.payload
+ const messageId = message.messageId || this.generateMessageId()
+ this.sendAcknowledgment(messageId)
+ try {
+ if (!window.openai?.callTool) {
+ throw new Error('Tool calling is not supported in this environment')
+ }
+ const result = await this.withTimeout(
+ window.openai.callTool(toolName, params),
+ messageId,
+ )
+ this.sendSuccessResponse(messageId, result)
+ } catch (error) {
+ this.sendErrorResponse(messageId, error)
+ }
+ }
+ /**
+ * Handle 'prompt' message - send followup turn
+ */
+ async handlePromptMessage(message) {
+ if (message.type !== 'prompt') return
+ const prompt = message.payload.prompt
+ const messageId = message.messageId || this.generateMessageId()
+ this.sendAcknowledgment(messageId)
+ try {
+ if (!window.openai?.sendFollowUpMessage) {
+ throw new Error('Followup turns are not supported in this environment')
+ }
+ await this.withTimeout(
+ window.openai.sendFollowUpMessage({ prompt }),
+ messageId,
+ )
+ this.sendSuccessResponse(messageId, { success: true })
+ } catch (error) {
+ this.sendErrorResponse(messageId, error)
+ }
+ }
+ /**
+ * Handle 'intent' message - convert to prompt or ignore based on config
+ */
+ async handleIntentMessage(message) {
+ if (message.type !== 'intent') return
+ const messageId = message.messageId || this.generateMessageId()
+ this.sendAcknowledgment(messageId)
+ if (this.config.intentHandling === 'ignore') {
+ this.config.logger.log(
+ '[MCPUI-Apps SDK Adapter] Intent ignored:',
+ message.payload.intent,
+ )
+ this.sendSuccessResponse(messageId, { ignored: true })
+ return
+ }
+ const { intent, params } = message.payload
+ const prompt = `${intent}${params ? ': ' + JSON.stringify(params) : ''}`
+ try {
+ if (!window.openai?.sendFollowUpMessage) {
+ throw new Error('Followup turns are not supported in this environment')
+ }
+ await this.withTimeout(
+ window.openai.sendFollowUpMessage({ prompt }),
+ messageId,
+ )
+ this.sendSuccessResponse(messageId, { success: true })
+ } catch (error) {
+ this.sendErrorResponse(messageId, error)
+ }
+ }
+ /**
+ * Handle 'notify' message - log only
+ */
+ async handleNotifyMessage(message) {
+ if (message.type !== 'notify') return
+ const messageId = message.messageId || this.generateMessageId()
+ this.config.logger.log(
+ '[MCPUI-Apps SDK Adapter] Notification:',
+ message.payload.message,
+ )
+ this.sendAcknowledgment(messageId)
+ this.sendSuccessResponse(messageId, { acknowledged: true })
+ }
+ /**
+ * Handle 'link' message - not supported in Apps SDK environments
+ */
+ async handleLinkMessage(message) {
+ if (message.type !== 'link') return
+ const messageId = message.messageId || this.generateMessageId()
+ this.sendAcknowledgment(messageId)
+ this.sendErrorResponse(
+ messageId,
+ new Error('Navigation is not supported in Apps SDK environment'),
+ )
+ }
+ /**
+ * Handle size change - no-op in Apps SDK environment
+ */
+ handleSizeChange(message) {
+ this.config.logger.debug(
+ '[MCPUI-Apps SDK Adapter] Size change requested (no-op in Apps SDK):',
+ message.payload,
+ )
+ }
+ /**
+ * Handle generic data request
+ */
+ handleRequestData(message) {
+ const messageId = message.messageId || this.generateMessageId()
+ this.sendAcknowledgment(messageId)
+ this.sendErrorResponse(
+ messageId,
+ new Error('Generic data requests not yet implemented'),
+ )
+ }
+ /**
+ * Setup listeners for Apps SDK events
+ */
+ setupAppsSdkEventListeners() {
+ window.addEventListener('openai:set_globals', () => {
+ this.config.logger.debug('[MCPUI-Apps SDK Adapter] Globals updated')
+ this.sendRenderData()
+ })
+ }
+ /**
+ * Gather render data from Apps SDK and send to widget
+ */
+ sendRenderData(requestMessageId) {
+ if (!window.openai) return
+ const renderData = {
+ toolInput: window.openai.toolInput,
+ toolOutput: window.openai.toolOutput,
+ widgetState: window.openai.widgetState,
+ locale: window.openai.locale || 'en-US',
+ theme: window.openai.theme || 'light',
+ displayMode: window.openai.displayMode || 'inline',
+ maxHeight: window.openai.maxHeight,
+ }
+ this.dispatchMessageToIframe({
+ type: 'ui-lifecycle-iframe-render-data',
+ messageId: requestMessageId,
+ payload: { renderData },
+ })
+ }
+ /**
+ * Send acknowledgment for a message
+ */
+ sendAcknowledgment(messageId) {
+ this.dispatchMessageToIframe({
+ type: 'ui-message-received',
+ payload: { messageId },
+ })
+ }
+ /**
+ * Send success response
+ */
+ sendSuccessResponse(messageId, response) {
+ this.dispatchMessageToIframe({
+ type: 'ui-message-response',
+ payload: { messageId, response },
+ })
+ }
+ /**
+ * Send error response
+ */
+ sendErrorResponse(messageId, error) {
+ const errorObj =
+ error instanceof Error
+ ? { message: error.message, name: error.name }
+ : { message: String(error) }
+ this.dispatchMessageToIframe({
+ type: 'ui-message-response',
+ payload: { messageId, error: errorObj },
+ })
+ }
+ /**
+ * Dispatch a MessageEvent to the iframe (widget)
+ * Simulates messages that would normally come from the parent/host
+ */
+ dispatchMessageToIframe(data) {
+ const event = new MessageEvent('message', {
+ data,
+ origin: this.config.hostOrigin,
+ source: null,
+ })
+ window.dispatchEvent(event)
+ }
+ /**
+ * Wrap a promise with timeout
+ */
+ async withTimeout(promise, requestId) {
+ return new Promise((resolve, reject) => {
+ const timeoutId = setTimeout(() => {
+ this.pendingRequests.delete(requestId)
+ reject(new Error(`Request timed out after ${this.config.timeout}ms`))
+ }, this.config.timeout)
+ this.pendingRequests.set(requestId, {
+ messageId: requestId,
+ type: 'generic',
+ resolve,
+ reject,
+ timeoutId,
+ })
+ promise
+ .then((result) => {
+ clearTimeout(timeoutId)
+ this.pendingRequests.delete(requestId)
+ resolve(result)
+ })
+ .catch((error) => {
+ clearTimeout(timeoutId)
+ this.pendingRequests.delete(requestId)
+ reject(error)
+ })
+ })
+ }
+ /**
+ * Generate a unique message ID
+ */
+ generateMessageId() {
+ return `adapter-${Date.now()}-${++this.messageIdCounter}`
+ }
+}
+var adapterInstance = null
+function initAdapter(config) {
+ if (adapterInstance) {
+ console.warn('[MCPUI-Apps SDK Adapter] Adapter already initialized')
+ return true
+ }
+ adapterInstance = new MCPUIAppsSdkAdapter(config)
+ return adapterInstance.install()
+}
+function uninstallAdapter() {
+ if (adapterInstance) {
+ adapterInstance.uninstall()
+ adapterInstance = null
+ }
+}
+if (
+ typeof window !== 'undefined' &&
+ !window.MCP_APPSSDK_ADAPTER_NO_AUTO_INSTALL
+) {
+ initAdapter()
+}
diff --git a/app/utils/mcp.ts b/app/utils/mcp.ts
index 823dc0f..b2b1624 100644
--- a/app/utils/mcp.ts
+++ b/app/utils/mcp.ts
@@ -1,3 +1,5 @@
+import '#app/utils/mcp-ui-compat.client.ts'
+
import { useEffect } from 'react'
import { type z } from 'zod'
@@ -56,16 +58,6 @@ function sendMcpMessage(
payload: McpMessageTypes[TypeType],
options: MessageOptions = {},
): McpMessageReturnType {
- debugger
- // if (type === 'tool') {
- // // Goose does not currentlly support tool calls, so change this to a prompt
- // const { toolName, params } = payload as McpMessageTypes['tool']
- // type = 'prompt' as TypeType
- // payload = {
- // prompt: `Please call the tool ${toolName} with the following parameters: ${JSON.stringify(params)}`,
- // } as McpMessageTypes[TypeType]
- // }
-
const { signal: givenSignal, schema, timeoutMs = 3_000 } = options
const timeoutSignal =
typeof timeoutMs === 'number' ? AbortSignal.timeout(timeoutMs) : undefined
@@ -91,6 +83,20 @@ function sendMcpMessage(
}
console.log('posting to parent', { type, messageId, payload })
+ if (type === 'tool') {
+ return resolve(
+ // @ts-expect-error ... we'll make this great eventually
+ window.openai.callTool(payload.toolName, payload.params).then((r) => {
+ // @ts-expect-error ... we'll make this great eventually
+ void window.openai.sendFollowUpMessage({
+ // @ts-expect-error ... we'll make this great eventually
+ prompt: `I have called ${payload.toolName} with params ${JSON.stringify(payload.params)} and got the following response: ${JSON.stringify(r)}`,
+ })
+ return r
+ }),
+ )
+ }
+
window.parent.postMessage({ type, messageId, payload }, '*')
function handleMessage(event: MessageEvent) {
@@ -121,68 +127,42 @@ function sendMcpMessage(
export { sendMcpMessage }
-// Module-level queue for render data events
-const renderDataQueue: Array<{ type: string; payload: any }> = []
-
-// Set up global listener immediately when module loads (only in the client)
-if (typeof document !== 'undefined') {
- window.addEventListener('message', (event) => {
- if (event.data?.type === 'ui-lifecycle-iframe-render-data') {
- renderDataQueue.push(event.data)
- }
- })
-}
-
export function waitForRenderData(
schema: z.ZodSchema,
- opts: { signal?: AbortSignal; timeoutMs?: number } = {},
): Promise {
- const { signal: givenSignal, timeoutMs = 3_000 } = opts
- const timeoutSignal =
- typeof timeoutMs === 'number' ? AbortSignal.timeout(timeoutMs) : undefined
-
- const signals = [givenSignal, timeoutSignal].filter(Boolean)
- const signal = AbortSignal.any(signals)
-
- return new Promise((resolve, reject) => {
- // Check if we already received the data
- const queuedEvent = renderDataQueue.find(
- (event) => event.type === 'ui-lifecycle-iframe-render-data',
- )
- if (queuedEvent) {
- const result = schema.safeParse(queuedEvent.payload.renderData)
- return result.success ? resolve(result.data) : reject(result.error)
+ let toolOutput = window.openai?.toolOutput
+ if (toolOutput) {
+ const parseResult = schema.safeParse({ toolOutput })
+ if (parseResult.success) {
+ return Promise.resolve(parseResult.data)
}
-
- // Otherwise, set up the normal listening logic
-
- function cleanup() {
- window.removeEventListener('message', handleMessage)
- signal.removeEventListener?.('abort', onAbort as EventListener)
- }
-
- function onAbort() {
- cleanup()
- const reason =
- (signal as any).reason ??
- new DOMException('Timed out waiting for render data', 'TimeoutError')
- reject(reason)
- }
-
- function handleMessage(event: MessageEvent) {
- if (event.data?.type !== 'ui-lifecycle-iframe-render-data') return
-
- const result = schema.safeParse(event.data.payload)
- cleanup()
- return result.success ? resolve(result.data) : reject(result.error)
- }
-
- signal.addEventListener('abort', onAbort, { once: true })
- window.addEventListener('message', handleMessage, {
- once: true,
- signal,
+ throw new Error(parseResult.error.message)
+ }
+
+ return new Promise((resolve, reject) => {
+ Object.defineProperty(window.openai, 'toolOutput', {
+ get() {
+ return toolOutput
+ },
+ set(newValue: any) {
+ toolOutput = newValue
+ const parseResult = schema.safeParse({ toolOutput })
+ if (parseResult.success) {
+ resolve(parseResult.data)
+ } else {
+ reject(new Error(parseResult.error.message))
+ }
+ },
+ configurable: true,
+ enumerable: true,
})
-
- if (signal.aborted) onAbort()
})
}
+
+declare global {
+ interface Window {
+ openai?: {
+ toolOutput?: any
+ }
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 086283b..040b7ca 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,25 +12,26 @@
"@epic-web/invariant": "^1.0.0",
"@epic-web/totp": "^4.0.1",
"@mcp-ui/server": "^5.11.0",
- "@modelcontextprotocol/sdk": "^1.19.1",
+ "@modelcontextprotocol/sdk": "^1.20.0",
"agents": "https://pkg.pr.new/cloudflare/agents@485",
"isbot": "^5.1.31",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-error-boundary": "^6.0.0",
- "react-router": "^7.9.3",
+ "react-router": "^7.9.4",
"zod": "^3.25.67"
},
"devDependencies": {
- "@cloudflare/vite-plugin": "^1.13.10",
+ "@cloudflare/vite-plugin": "^1.13.12",
"@epic-web/config": "^1.21.3",
"@modelcontextprotocol/inspector": "^0.17.0",
- "@react-router/dev": "^7.9.3",
+ "@react-router/dev": "^7.9.4",
"@tailwindcss/vite": "^4.1.14",
- "@types/node": "^24.6.2",
- "@types/react": "^19.2.0",
- "@types/react-dom": "^19.2.0",
- "eslint": "^9.36.0",
+ "@types/node": "^24.7.1",
+ "@types/react": "^19.2.2",
+ "@types/react-dom": "^19.2.1",
+ "cross-env": "^10.1.0",
+ "eslint": "^9.37.0",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.14",
@@ -38,7 +39,7 @@
"vite": "^7.1.9",
"vite-plugin-devtools-json": "^1.0.0",
"vite-tsconfig-paths": "^5.1.4",
- "wrangler": "^4.42.0"
+ "wrangler": "^4.42.2"
}
},
"node_modules/@adraffy/ens-normalize": {
@@ -704,9 +705,9 @@
}
},
"node_modules/@cloudflare/unenv-preset": {
- "version": "2.7.6",
- "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.6.tgz",
- "integrity": "sha512-ykG2nd3trk6jbknRCH69xL3RpGLLbKCrbTbWSOvKEq7s4jH06yLrQlRr/q9IU+dK9p1JY1EXqhFK7VG5KqhzmQ==",
+ "version": "2.7.7",
+ "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.7.tgz",
+ "integrity": "sha512-HtZuh166y0Olbj9bqqySckz0Rw9uHjggJeoGbDx5x+sgezBXlxO6tQSig2RZw5tgObF8mWI8zaPvQMkQZtAODw==",
"dev": true,
"license": "MIT OR Apache-2.0",
"peerDependencies": {
@@ -720,25 +721,25 @@
}
},
"node_modules/@cloudflare/vite-plugin": {
- "version": "1.13.10",
- "resolved": "https://registry.npmjs.org/@cloudflare/vite-plugin/-/vite-plugin-1.13.10.tgz",
- "integrity": "sha512-zQaCbzGDAMhjZqXfulpUgBL/D4qsoP1oHVk2LyseKJ47PMq2cHWnbISOi3RONvKxpGyct7ACjA4JEhbFlu5GNQ==",
+ "version": "1.13.12",
+ "resolved": "https://registry.npmjs.org/@cloudflare/vite-plugin/-/vite-plugin-1.13.12.tgz",
+ "integrity": "sha512-JEcrUF1uXxMQfp+RcGw7ov5K8uOGX0arFYdyO3QfHrjWspHnsw0zOYL+4083itEInRVnZjS5LunpsNpxSVbCEw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@cloudflare/unenv-preset": "2.7.6",
+ "@cloudflare/unenv-preset": "2.7.7",
"@remix-run/node-fetch-server": "^0.8.0",
"get-port": "^7.1.0",
- "miniflare": "4.20251001.0",
+ "miniflare": "4.20251008.0",
"picocolors": "^1.1.1",
"tinyglobby": "^0.2.12",
"unenv": "2.0.0-rc.21",
- "wrangler": "4.42.0",
+ "wrangler": "4.42.2",
"ws": "8.18.0"
},
"peerDependencies": {
"vite": "^6.1.0 || ^7.0.0",
- "wrangler": "^4.42.0"
+ "wrangler": "^4.42.2"
}
},
"node_modules/@cloudflare/vite-plugin/node_modules/ws": {
@@ -764,9 +765,9 @@
}
},
"node_modules/@cloudflare/workerd-darwin-64": {
- "version": "1.20251001.0",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20251001.0.tgz",
- "integrity": "sha512-y1ST/cCscaRewWRnsHZdWbgiLJbki5UMGd0hMo/FLqjlztwPeDgQ5CGm5jMiCDdw/IBCpWxEukftPYR34rWNog==",
+ "version": "1.20251008.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20251008.0.tgz",
+ "integrity": "sha512-yph0H+8mMOK5Z9oDwjb8rI96oTVt4no5lZ43aorcbzsWG9VUIaXSXlBBoB3von6p4YCRW+J3n36fBM9XZ6TLaA==",
"cpu": [
"x64"
],
@@ -781,9 +782,9 @@
}
},
"node_modules/@cloudflare/workerd-darwin-arm64": {
- "version": "1.20251001.0",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20251001.0.tgz",
- "integrity": "sha512-+z4QHHZ/Yix82zLFYS+ZS2UV09IENFPwDCEKUWfnrM9Km2jOOW3Ua4hJNob1EgQUYs8fFZo7k5O/tpwxMsSbbQ==",
+ "version": "1.20251008.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20251008.0.tgz",
+ "integrity": "sha512-Yc4lMGSbM4AEtYRpyDpmk77MsHb6X2BSwJgMgGsLVPmckM7ZHivZkJChfcNQjZ/MGR6nkhYc4iF6TcVS+UMEVw==",
"cpu": [
"arm64"
],
@@ -798,9 +799,9 @@
}
},
"node_modules/@cloudflare/workerd-linux-64": {
- "version": "1.20251001.0",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20251001.0.tgz",
- "integrity": "sha512-hGS+O2V9Mm2XjJUaB9ZHMA5asDUaDjKko42e+accbew0PQR7zrAl1afdII6hMqCLV4tk4GAjvhv281pN4g48rg==",
+ "version": "1.20251008.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20251008.0.tgz",
+ "integrity": "sha512-AjoQnylw4/5G6SmfhZRsli7EuIK7ZMhmbxtU0jkpciTlVV8H01OsFOgS1d8zaTXMfkWamEfMouy8oH/L7B9YcQ==",
"cpu": [
"x64"
],
@@ -815,9 +816,9 @@
}
},
"node_modules/@cloudflare/workerd-linux-arm64": {
- "version": "1.20251001.0",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20251001.0.tgz",
- "integrity": "sha512-QYaMK+pRgt28N7CX1JlJ+ToegJF9LxzqdT7MjWqPgVj9D2WTyIhBVYl3wYjJRcgOlnn+DRt42+li4T64CPEeuA==",
+ "version": "1.20251008.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20251008.0.tgz",
+ "integrity": "sha512-hRy9yyvzVq1HsqHZUmFkAr0C8JGjAD/PeeVEGCKL3jln3M9sNCKIrbDXiL+efe+EwajJNNlDxpO+s30uVWVaRg==",
"cpu": [
"arm64"
],
@@ -832,9 +833,9 @@
}
},
"node_modules/@cloudflare/workerd-windows-64": {
- "version": "1.20251001.0",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20251001.0.tgz",
- "integrity": "sha512-ospnDR/FlyRvrv9DSHuxDAXmzEBLDUiAHQrQHda1iUH9HqxnNQ8giz9VlPfq7NIRc7bQ1ZdIYPGLJOY4Q366Ng==",
+ "version": "1.20251008.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20251008.0.tgz",
+ "integrity": "sha512-Gm0RR+ehfNMsScn2pUcn3N9PDUpy7FyvV9ecHEyclKttvztyFOcmsF14bxEaSVv7iM4TxWEBn1rclmYHxDM4ow==",
"cpu": [
"x64"
],
@@ -1527,19 +1528,22 @@
}
},
"node_modules/@eslint/config-helpers": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz",
- "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==",
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz",
+ "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==",
"dev": true,
"license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.16.0"
+ },
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/core": {
- "version": "0.15.2",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
- "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
+ "version": "0.16.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
+ "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -1611,9 +1615,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.36.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz",
- "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==",
+ "version": "9.37.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz",
+ "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1634,13 +1638,13 @@
}
},
"node_modules/@eslint/plugin-kit": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
- "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz",
+ "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@eslint/core": "^0.15.2",
+ "@eslint/core": "^0.16.0",
"levn": "^0.4.1"
},
"engines": {
@@ -3100,9 +3104,9 @@
}
},
"node_modules/@modelcontextprotocol/sdk": {
- "version": "1.19.1",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.19.1.tgz",
- "integrity": "sha512-3Y2h3MZKjec1eAqSTBclATlX+AbC6n1LgfVzRMJLt3v6w0RCYgwLrjbxPDbhsYHt6Wdqc/aCceNJYgj448ELQQ==",
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.0.tgz",
+ "integrity": "sha512-kOQ4+fHuT4KbR2iq2IjeV32HiihueuOf1vJkq18z08CLZ1UQrTc8BXJpVfxZkq45+inLLD+D4xx4nBjUelJa4Q==",
"license": "MIT",
"dependencies": {
"ajv": "^6.12.6",
@@ -4238,9 +4242,9 @@
"license": "MIT"
},
"node_modules/@react-router/dev": {
- "version": "7.9.3",
- "resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.9.3.tgz",
- "integrity": "sha512-oPaO+OpvCo/rNTJrRipHSp31/K4It19PE5A24x21FlYlemPTe3fbGX/kyC2+8au/abXbvzNHfRbuIBD/rfojmA==",
+ "version": "7.9.4",
+ "resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.9.4.tgz",
+ "integrity": "sha512-bLs6DjKMJExT7Y57EBx25hkeGGUla3pURxvOn15IN8Mmaw2+euDtBUX9+OFrAPsAzD1xIj6+2HNLXlFH/LB86Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4252,7 +4256,7 @@
"@babel/traverse": "^7.27.7",
"@babel/types": "^7.27.7",
"@npmcli/package-json": "^4.0.1",
- "@react-router/node": "7.9.3",
+ "@react-router/node": "7.9.4",
"@remix-run/node-fetch-server": "^0.9.0",
"arg": "^5.0.1",
"babel-dead-code-elimination": "^1.0.6",
@@ -4269,7 +4273,7 @@
"react-refresh": "^0.14.0",
"semver": "^7.3.7",
"tinyglobby": "^0.2.14",
- "valibot": "^0.41.0",
+ "valibot": "^1.1.0",
"vite-node": "^3.2.2"
},
"bin": {
@@ -4279,9 +4283,9 @@
"node": ">=20.0.0"
},
"peerDependencies": {
- "@react-router/serve": "^7.9.3",
+ "@react-router/serve": "^7.9.4",
"@vitejs/plugin-rsc": "*",
- "react-router": "^7.9.3",
+ "react-router": "^7.9.4",
"typescript": "^5.1.0",
"vite": "^5.1.0 || ^6.0.0 || ^7.0.0",
"wrangler": "^3.28.2 || ^4.0.0"
@@ -4323,9 +4327,9 @@
"license": "MIT"
},
"node_modules/@react-router/node": {
- "version": "7.9.3",
- "resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.9.3.tgz",
- "integrity": "sha512-+OvWxPPUgouOshw85QlG0J6yFJM0GMCCpXqPj38IcveeFLlP7ppOAEkOi7RBFrDvg7vSUtCEBDnsbuDCvxUPJg==",
+ "version": "7.9.4",
+ "resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.9.4.tgz",
+ "integrity": "sha512-sdeDNRaqAB71BR2hPlhcQbPbrXh8uGJUjLVc+NpRiPsQbv6B8UvIucN4IX9YGVJkw3UxVQBn2vPSwxACAck32Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4335,7 +4339,7 @@
"node": ">=20.0.0"
},
"peerDependencies": {
- "react-router": "7.9.3",
+ "react-router": "7.9.4",
"typescript": "^5.1.0"
},
"peerDependenciesMeta": {
@@ -6793,19 +6797,19 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "24.6.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz",
- "integrity": "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==",
+ "version": "24.7.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.1.tgz",
+ "integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~7.13.0"
+ "undici-types": "~7.14.0"
}
},
"node_modules/@types/react": {
- "version": "19.2.0",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz",
- "integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==",
+ "version": "19.2.2",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
+ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6813,9 +6817,9 @@
}
},
"node_modules/@types/react-dom": {
- "version": "19.2.0",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.0.tgz",
- "integrity": "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg==",
+ "version": "19.2.1",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz",
+ "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -9100,6 +9104,24 @@
"node": ">=18"
}
},
+ "node_modules/cross-env": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
+ "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@epic-web/invariant": "^1.0.0",
+ "cross-spawn": "^7.0.6"
+ },
+ "bin": {
+ "cross-env": "dist/bin/cross-env.js",
+ "cross-env-shell": "dist/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/cross-fetch": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
@@ -9908,20 +9930,20 @@
}
},
"node_modules/eslint": {
- "version": "9.36.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz",
- "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
+ "version": "9.37.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz",
+ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.21.0",
- "@eslint/config-helpers": "^0.3.1",
- "@eslint/core": "^0.15.2",
+ "@eslint/config-helpers": "^0.4.0",
+ "@eslint/core": "^0.16.0",
"@eslint/eslintrc": "^3.3.1",
- "@eslint/js": "9.36.0",
- "@eslint/plugin-kit": "^0.3.5",
+ "@eslint/js": "9.37.0",
+ "@eslint/plugin-kit": "^0.4.0",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
@@ -12652,9 +12674,9 @@
}
},
"node_modules/miniflare": {
- "version": "4.20251001.0",
- "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20251001.0.tgz",
- "integrity": "sha512-OHd31D2LT8JH+85nVXClV0Z18jxirCohzKNAcZs/fgt4mIkUDtidX3VqR3ovAM0jWooNxrFhB9NSs3iDbiJF7Q==",
+ "version": "4.20251008.0",
+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20251008.0.tgz",
+ "integrity": "sha512-sKCNYNzXG6l8qg0Oo7y8WcDKcpbgw0qwZsxNpdZilFTR4EavRow2TlcwuPSVN99jqAjhz0M4VXvTdSGdtJ2VfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12666,7 +12688,7 @@
"sharp": "^0.33.5",
"stoppable": "1.1.0",
"undici": "7.14.0",
- "workerd": "1.20251001.0",
+ "workerd": "1.20251008.0",
"ws": "8.18.0",
"youch": "4.1.0-beta.10",
"zod": "3.22.3"
@@ -14174,9 +14196,9 @@
}
},
"node_modules/react-router": {
- "version": "7.9.3",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.3.tgz",
- "integrity": "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==",
+ "version": "7.9.4",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz",
+ "integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -15890,9 +15912,9 @@
}
},
"node_modules/undici-types": {
- "version": "7.13.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz",
- "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==",
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
+ "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
"license": "MIT"
},
"node_modules/unenv": {
@@ -16203,9 +16225,9 @@
"license": "MIT"
},
"node_modules/valibot": {
- "version": "0.41.0",
- "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.41.0.tgz",
- "integrity": "sha512-igDBb8CTYr8YTQlOKgaN9nSS0Be7z+WRuaeYqGf3Cjz3aKmSnqEmYnkfVjzIuumGqfHpa3fLIvMEAfhrpqN8ng==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.1.0.tgz",
+ "integrity": "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -16651,9 +16673,9 @@
}
},
"node_modules/workerd": {
- "version": "1.20251001.0",
- "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20251001.0.tgz",
- "integrity": "sha512-oT/K4YWNhmwpVmGeaHNmF7mLRfgjszlVr7lJtpS4jx5khmxmMzWZEEQRrJEpgzeHP6DOq9qWLPNT0bjMK7TchQ==",
+ "version": "1.20251008.0",
+ "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20251008.0.tgz",
+ "integrity": "sha512-HwaJmXO3M1r4S8x2ea2vy8Rw/y/38HRQuK/gNDRQ7w9cJXn6xSl1sIIqKCffULSUjul3wV3I3Nd/GfbmsRReEA==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
@@ -16664,28 +16686,28 @@
"node": ">=16"
},
"optionalDependencies": {
- "@cloudflare/workerd-darwin-64": "1.20251001.0",
- "@cloudflare/workerd-darwin-arm64": "1.20251001.0",
- "@cloudflare/workerd-linux-64": "1.20251001.0",
- "@cloudflare/workerd-linux-arm64": "1.20251001.0",
- "@cloudflare/workerd-windows-64": "1.20251001.0"
+ "@cloudflare/workerd-darwin-64": "1.20251008.0",
+ "@cloudflare/workerd-darwin-arm64": "1.20251008.0",
+ "@cloudflare/workerd-linux-64": "1.20251008.0",
+ "@cloudflare/workerd-linux-arm64": "1.20251008.0",
+ "@cloudflare/workerd-windows-64": "1.20251008.0"
}
},
"node_modules/wrangler": {
- "version": "4.42.0",
- "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.42.0.tgz",
- "integrity": "sha512-OZXiUSfGD66OVkncDbjZtqrsH6bWPRQMYc6RmMbkzYm/lEvJ8lvARKcqDgEyq8zDAgJAivlMQLyPtKQoVjQ/4g==",
+ "version": "4.42.2",
+ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.42.2.tgz",
+ "integrity": "sha512-1iTnbjB4F12KSP1zbfxQL495xarS+vdrZnulQP2SEcAxDTUGn7N9zk1O2WtFOc+Fhcgl+9/sdz/4AL9pF34Pwg==",
"dev": true,
"license": "MIT OR Apache-2.0",
"dependencies": {
"@cloudflare/kv-asset-handler": "0.4.0",
- "@cloudflare/unenv-preset": "2.7.6",
+ "@cloudflare/unenv-preset": "2.7.7",
"blake3-wasm": "2.1.5",
"esbuild": "0.25.4",
- "miniflare": "4.20251001.0",
+ "miniflare": "4.20251008.0",
"path-to-regexp": "6.3.0",
"unenv": "2.0.0-rc.21",
- "workerd": "1.20251001.0"
+ "workerd": "1.20251008.0"
},
"bin": {
"wrangler": "bin/wrangler.js",
@@ -16698,7 +16720,7 @@
"fsevents": "~2.3.2"
},
"peerDependencies": {
- "@cloudflare/workers-types": "^4.20251001.0"
+ "@cloudflare/workers-types": "^4.20251008.0"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
diff --git a/package.json b/package.json
index ad0a755..e827329 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,9 @@
},
"scripts": {
"build": "react-router build",
+ "build:staging": "cross-env CLOUDFLARE_ENV=staging react-router build",
"deploy": "npm run build && wrangler deploy",
+ "deploy:staging": "npm run build:staging && wrangler deploy",
"dev": "react-router dev",
"format": "prettier --write .",
"lint": "eslint .",
@@ -25,25 +27,26 @@
"@epic-web/invariant": "^1.0.0",
"@epic-web/totp": "^4.0.1",
"@mcp-ui/server": "^5.11.0",
- "@modelcontextprotocol/sdk": "^1.19.1",
+ "@modelcontextprotocol/sdk": "^1.20.0",
"agents": "https://pkg.pr.new/cloudflare/agents@485",
"isbot": "^5.1.31",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-error-boundary": "^6.0.0",
- "react-router": "^7.9.3",
+ "react-router": "^7.9.4",
"zod": "^3.25.67"
},
"devDependencies": {
- "@cloudflare/vite-plugin": "^1.13.10",
+ "@cloudflare/vite-plugin": "^1.13.12",
"@epic-web/config": "^1.21.3",
"@modelcontextprotocol/inspector": "^0.17.0",
- "@react-router/dev": "^7.9.3",
+ "@react-router/dev": "^7.9.4",
"@tailwindcss/vite": "^4.1.14",
- "@types/node": "^24.6.2",
- "@types/react": "^19.2.0",
- "@types/react-dom": "^19.2.0",
- "eslint": "^9.36.0",
+ "@types/node": "^24.7.1",
+ "@types/react": "^19.2.2",
+ "@types/react-dom": "^19.2.1",
+ "cross-env": "^10.1.0",
+ "eslint": "^9.37.0",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.14",
@@ -51,7 +54,7 @@
"vite": "^7.1.9",
"vite-plugin-devtools-json": "^1.0.0",
"vite-tsconfig-paths": "^5.1.4",
- "wrangler": "^4.42.0"
+ "wrangler": "^4.42.2"
},
"prettier": "@epic-web/config/prettier",
"license": "GPL-3.0-only"
diff --git a/public/_headers b/public/_headers
new file mode 100644
index 0000000..53c121e
--- /dev/null
+++ b/public/_headers
@@ -0,0 +1,5 @@
+/*
+ Access-Control-Allow-Origin: *
+ Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
+ Access-Control-Allow-Headers: *
+ Access-Control-Max-Age: 86400
diff --git a/react-router.config.ts b/react-router.config.ts
index d115e54..8160f32 100644
--- a/react-router.config.ts
+++ b/react-router.config.ts
@@ -2,6 +2,7 @@ import { type Config } from '@react-router/dev/config'
export default {
ssr: true,
+ routeDiscovery: { mode: 'initial' },
future: {
unstable_viteEnvironmentApi: true,
},
diff --git a/types/worker-configuration.d.ts b/types/worker-configuration.d.ts
index 869bd42..9b76c61 100644
--- a/types/worker-configuration.d.ts
+++ b/types/worker-configuration.d.ts
@@ -1,6 +1,6 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types ./types/worker-configuration.d.ts` (hash: 808cf267dca29f8c048696dc60b21f13)
-// Runtime types generated with workerd@1.20251001.0 2025-04-17 nodejs_compat
+// Runtime types generated with workerd@1.20251008.0 2025-04-17 nodejs_compat
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("../worker/index");
@@ -7437,6 +7437,10 @@ type MediaTransformationOutputOptions = {
* Duration for video clips, audio extraction, and spritesheet generation (e.g. '5s').
*/
duration?: string;
+ /**
+ * Number of frames in the spritesheet.
+ */
+ imageCount?: number;
/**
* Output format for the generated media.
*/
diff --git a/vite.config.ts b/vite.config.ts
index 5b43f04..099e46b 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -6,6 +6,13 @@ import devtoolsJson from 'vite-plugin-devtools-json'
import tsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
+ base:
+ process.env.NODE_ENV === 'production'
+ ? process.env.CLOUDFLARE_ENV === 'staging'
+ ? 'https://epic-me-mcp-staging.kentcdodds.workers.dev/'
+ : 'https://epic-me-mcp.kentcdodds.workers.dev/'
+ : undefined,
+ define: { BUILD_TIMESTAMP: JSON.stringify(Date.now()) },
server: {
port: 8877,
},
@@ -20,7 +27,10 @@ export default defineConfig({
if (id.includes('+types/')) return 'export {}'
},
},
- cloudflare({ viteEnvironment: { name: 'ssr' } }),
+ cloudflare({
+ viteEnvironment: { name: 'ssr' },
+ experimental: { headersAndRedirectsDevModeSupport: true },
+ }),
tailwindcss(),
reactRouter(),
tsconfigPaths(),
diff --git a/worker/mcp/index.ts b/worker/mcp/index.ts
index a707af2..5e45b9e 100644
--- a/worker/mcp/index.ts
+++ b/worker/mcp/index.ts
@@ -12,6 +12,7 @@ import { DB } from '../db'
import { initializePrompts } from './prompts.ts'
import { initializeResources } from './resources.ts'
import { initializeTools } from './tools.ts'
+import { registerWidgets } from './widgets.ts'
type State = {}
export type Props = { grantId: string; grantUserId: string; baseUrl: string }
@@ -92,6 +93,7 @@ Always call \`whoami\` first. If unauthenticated: 1) \`authenticate\` with email
await initializeTools(this)
await initializeResources(this)
await initializePrompts(this)
+ await registerWidgets(this)
}
onStateUpdate(state: State | undefined, source: Connection | 'server') {
@@ -134,6 +136,9 @@ Always call \`whoami\` first. If unauthenticated: 1) \`authenticate\` with email
}
async updateAvailableItems() {
+ const supportsListUpdates = false
+ if (supportsListUpdates) return
+
const { grantId } = this.props ?? {}
if (!grantId) return
diff --git a/worker/mcp/tools.ts b/worker/mcp/tools.ts
index 12079c8..e0047aa 100644
--- a/worker/mcp/tools.ts
+++ b/worker/mcp/tools.ts
@@ -107,77 +107,77 @@ export async function initializeTools(agent: EpicMeMCP) {
)
agent.authenticatedTools.push(
- agent.server.registerTool(
- 'view_journal',
- {
- title: 'View Journal',
- description:
- 'View your journal entries in a beautiful, scrollable interface',
- annotations: {
- readOnlyHint: true,
- openWorldHint: false,
- } satisfies ToolAnnotations,
- },
- async () => {
- const user = await agent.requireUser()
- const baseUrl = agent.requireDomain()
- const uiUrl = new URL(`/ui/journal-viewer`, baseUrl)
- const entries = await agent.db.getEntries(user.id)
- return {
- content: [
- createText(
- `Here's your journal viewer. You can scroll through your entries and expand them to read the full content.`,
- ),
- createUIResource({
- uri: `ui://journal-viewer/${user.id}`,
- content: {
- type: 'externalUrl',
- iframeUrl: uiUrl.toString(),
- },
- encoding: 'text',
- uiMetadata: {
- 'initial-render-data': { entries },
- },
- }),
- ],
- }
- },
- ),
- agent.server.registerTool(
- 'view_entry',
- {
- title: 'View Entry',
- description: 'View a journal entry by ID visually',
- annotations: {
- readOnlyHint: true,
- openWorldHint: false,
- } satisfies ToolAnnotations,
- inputSchema: entryIdSchema,
- },
- async ({ id }) => {
- const user = await agent.requireUser()
- const baseUrl = agent.requireDomain()
- const iframeUrl = new URL('/ui/entry-viewer', baseUrl)
- const entry = await agent.db.getEntry(user.id, id)
- invariant(entry, `Entry with ID "${id}" not found`)
+ // agent.server.registerTool(
+ // 'view_journal',
+ // {
+ // title: 'View Journal',
+ // description:
+ // 'View your journal entries in a beautiful, scrollable interface',
+ // annotations: {
+ // readOnlyHint: true,
+ // openWorldHint: false,
+ // } satisfies ToolAnnotations,
+ // },
+ // async () => {
+ // const user = await agent.requireUser()
+ // const baseUrl = agent.requireDomain()
+ // const uiUrl = new URL(`/ui/journal-viewer`, baseUrl)
+ // const entries = await agent.db.getEntries(user.id)
+ // return {
+ // content: [
+ // createText(
+ // `Here's your journal viewer. You can scroll through your entries and expand them to read the full content.`,
+ // ),
+ // createUIResource({
+ // uri: `ui://journal-viewer/${user.id}`,
+ // content: {
+ // type: 'externalUrl',
+ // iframeUrl: uiUrl.toString(),
+ // },
+ // encoding: 'text',
+ // uiMetadata: {
+ // 'initial-render-data': { entries },
+ // },
+ // }),
+ // ],
+ // }
+ // },
+ // ),
+ // agent.server.registerTool(
+ // 'view_entry',
+ // {
+ // title: 'View Entry',
+ // description: 'View a journal entry by ID visually',
+ // annotations: {
+ // readOnlyHint: true,
+ // openWorldHint: false,
+ // } satisfies ToolAnnotations,
+ // inputSchema: entryIdSchema,
+ // },
+ // async ({ id }) => {
+ // const user = await agent.requireUser()
+ // const baseUrl = agent.requireDomain()
+ // const iframeUrl = new URL('/ui/entry-viewer', baseUrl)
+ // const entry = await agent.db.getEntry(user.id, id)
+ // invariant(entry, `Entry with ID "${id}" not found`)
- return {
- content: [
- createUIResource({
- uri: `ui://view-entry/${id}`,
- content: {
- type: 'externalUrl',
- iframeUrl: iframeUrl.toString(),
- },
- encoding: 'text',
- uiMetadata: {
- 'initial-render-data': { entry },
- },
- }),
- ],
- }
- },
- ),
+ // return {
+ // content: [
+ // createUIResource({
+ // uri: `ui://view-entry/${id}`,
+ // content: {
+ // type: 'externalUrl',
+ // iframeUrl: iframeUrl.toString(),
+ // },
+ // encoding: 'text',
+ // uiMetadata: {
+ // 'initial-render-data': { entry },
+ // },
+ // }),
+ // ],
+ // }
+ // },
+ // ),
agent.server.registerTool(
'whoami',
{
@@ -340,39 +340,39 @@ export async function initializeTools(agent: EpicMeMCP) {
},
),
- agent.server.registerTool(
- 'update_entry',
- {
- title: 'Update Entry',
- description:
- 'Update a journal entry. Only provided fields will be updated.',
- annotations: {
- destructiveHint: false,
- idempotentHint: true,
- openWorldHint: false,
- } satisfies ToolAnnotations,
- inputSchema: updateEntryInputSchema,
- outputSchema: { entry: entryWithTagsSchema },
- },
- async ({ id, ...updates }) => {
- const user = await agent.requireUser()
- const existingEntry = await agent.db.getEntry(user.id, id)
- invariant(
- existingEntry,
- `Entry with ID "${id}" not found. Use list_entries to see all available entries.`,
- )
- const updatedEntry = await agent.db.updateEntry(user.id, id, updates)
- return {
- structuredContent: { entry: updatedEntry },
- content: [
- createText(
- `Entry "${updatedEntry.title}" (ID: ${id}) updated successfully`,
- ),
- createEntryResourceLink(updatedEntry),
- ],
- }
- },
- ),
+ // agent.server.registerTool(
+ // 'update_entry',
+ // {
+ // title: 'Update Entry',
+ // description:
+ // 'Update a journal entry. Only provided fields will be updated.',
+ // annotations: {
+ // destructiveHint: false,
+ // idempotentHint: true,
+ // openWorldHint: false,
+ // } satisfies ToolAnnotations,
+ // inputSchema: updateEntryInputSchema,
+ // outputSchema: { entry: entryWithTagsSchema },
+ // },
+ // async ({ id, ...updates }) => {
+ // const user = await agent.requireUser()
+ // const existingEntry = await agent.db.getEntry(user.id, id)
+ // invariant(
+ // existingEntry,
+ // `Entry with ID "${id}" not found. Use list_entries to see all available entries.`,
+ // )
+ // const updatedEntry = await agent.db.updateEntry(user.id, id, updates)
+ // return {
+ // structuredContent: { entry: updatedEntry },
+ // content: [
+ // createText(
+ // `Entry "${updatedEntry.title}" (ID: ${id}) updated successfully`,
+ // ),
+ // createEntryResourceLink(updatedEntry),
+ // ],
+ // }
+ // },
+ // ),
agent.server.registerTool(
'delete_entry',
{
@@ -521,35 +521,35 @@ export async function initializeTools(agent: EpicMeMCP) {
}
},
),
- agent.server.registerTool(
- 'update_tag',
- {
- title: 'Update Tag',
- description: 'Update a tag',
- annotations: {
- destructiveHint: false,
- idempotentHint: true,
- openWorldHint: false,
- } satisfies ToolAnnotations,
- inputSchema: updateTagInputSchema,
- outputSchema: { tag: tagSchema },
- },
- async ({ id, ...updates }) => {
- const user = await agent.requireUser()
- const updatedTag = await agent.db.updateTag(user.id, id, updates)
- const structuredContent = { tag: updatedTag }
- return {
- structuredContent,
- content: [
- createText(
- `Tag "${updatedTag.name}" (ID: ${id}) updated successfully`,
- ),
- createTagResourceLink(updatedTag),
- createText(structuredContent),
- ],
- }
- },
- ),
+ // agent.server.registerTool(
+ // 'update_tag',
+ // {
+ // title: 'Update Tag',
+ // description: 'Update a tag',
+ // annotations: {
+ // destructiveHint: false,
+ // idempotentHint: true,
+ // openWorldHint: false,
+ // } satisfies ToolAnnotations,
+ // inputSchema: updateTagInputSchema,
+ // outputSchema: { tag: tagSchema },
+ // },
+ // async ({ id, ...updates }) => {
+ // const user = await agent.requireUser()
+ // const updatedTag = await agent.db.updateTag(user.id, id, updates)
+ // const structuredContent = { tag: updatedTag }
+ // return {
+ // structuredContent,
+ // content: [
+ // createText(
+ // `Tag "${updatedTag.name}" (ID: ${id}) updated successfully`,
+ // ),
+ // createTagResourceLink(updatedTag),
+ // createText(structuredContent),
+ // ],
+ // }
+ // },
+ // ),
agent.server.registerTool(
'delete_tag',
{
diff --git a/worker/mcp/widgets.ts b/worker/mcp/widgets.ts
new file mode 100644
index 0000000..22dfd5d
--- /dev/null
+++ b/worker/mcp/widgets.ts
@@ -0,0 +1,122 @@
+import { invariant } from '@epic-web/invariant'
+import { type ZodRawShape, z } from 'zod'
+import { entryWithTagsSchema } from '#worker/db/schema.ts'
+import { type EpicMeMCP } from './index.ts'
+
+declare const BUILD_TIMESTAMP: string
+const version = BUILD_TIMESTAMP
+
+type WidgetOutput = {
+ inputSchema: Input
+ outputSchema: Output
+ getStructuredContent: (args: {
+ [Key in keyof Input]: z.infer
+ }) => Promise<{
+ [Key in keyof Output]: z.infer