From 7eed5c464e44daa11c12f89985e27622662e3f17 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Thu, 23 Apr 2026 12:44:37 +0200 Subject: [PATCH] add example-mcp-app: Spiceflow website + MCP server with interactive map UI Demonstrates how a single Spiceflow app can serve both a website (RSC pages with Tailwind) and an MCP server endpoint at POST /mcp. Architecture: - src/main.tsx: Spiceflow app with / and /about pages, plus a /mcp endpoint that adapts Web Request/Response to the MCP SDK StreamableHTTPServerTransport - src/mcp-server.ts: McpServer factory with geocode (OpenStreetMap Nominatim) and show-map (interactive Leaflet map) tools - src/app/mcp-app.tsx: client-only React app for the MCP App iframe UI, uses @modelcontextprotocol/ext-apps SDK for host communication and registers a navigate-to tool back to the LLM - mcp-app.html: HTML shell bundled into a single file via vite-plugin-singlefile - Two Vite configs: one for the Spiceflow website, one for the MCP UI bundle Build pipeline: pnpm build:mcp-ui bundles the iframe HTML first, then pnpm build runs the Spiceflow production build. The MCP server reads the pre-built HTML from dist-mcp-ui/ at runtime and serves it as a ui:// resource. Session: ses_2462ab72dffelMbT6WxWAkDW24 --- example-mcp-app/.gitignore | 3 + example-mcp-app/mcp-app.html | 38 ++++ example-mcp-app/package.json | 29 +++ example-mcp-app/src/app/mcp-app.tsx | 274 ++++++++++++++++++++++++++ example-mcp-app/src/globals.css | 1 + example-mcp-app/src/main.tsx | 201 +++++++++++++++++++ example-mcp-app/src/mcp-server.ts | 184 +++++++++++++++++ example-mcp-app/tsconfig.json | 16 ++ example-mcp-app/vite.config.ts | 15 ++ example-mcp-app/vite.mcp-ui.config.ts | 21 ++ pnpm-lock.yaml | 120 +++++++++++ 11 files changed, 902 insertions(+) create mode 100644 example-mcp-app/.gitignore create mode 100644 example-mcp-app/mcp-app.html create mode 100644 example-mcp-app/package.json create mode 100644 example-mcp-app/src/app/mcp-app.tsx create mode 100644 example-mcp-app/src/globals.css create mode 100644 example-mcp-app/src/main.tsx create mode 100644 example-mcp-app/src/mcp-server.ts create mode 100644 example-mcp-app/tsconfig.json create mode 100644 example-mcp-app/vite.config.ts create mode 100644 example-mcp-app/vite.mcp-ui.config.ts diff --git a/example-mcp-app/.gitignore b/example-mcp-app/.gitignore new file mode 100644 index 00000000..c1e6c232 --- /dev/null +++ b/example-mcp-app/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +dist-mcp-ui diff --git a/example-mcp-app/mcp-app.html b/example-mcp-app/mcp-app.html new file mode 100644 index 00000000..e3ad4e09 --- /dev/null +++ b/example-mcp-app/mcp-app.html @@ -0,0 +1,38 @@ + + + + + + Map MCP App + + + + +
+ + + diff --git a/example-mcp-app/package.json b/example-mcp-app/package.json new file mode 100644 index 00000000..efff97d5 --- /dev/null +++ b/example-mcp-app/package.json @@ -0,0 +1,29 @@ +{ + "name": "example-mcp-app", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "pnpm build:mcp-ui && vite build", + "build:mcp-ui": "INPUT=mcp-app.html vite build --config vite.mcp-ui.config.ts", + "start": "node dist/rsc/index.js" + }, + "dependencies": { + "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "@tailwindcss/vite": "^4.2.2", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "react": "19.2.4", + "react-dom": "19.2.4", + "spiceflow": "workspace:^", + "tailwindcss": "4.0.6", + "typescript": "5.7.3", + "vite": "^8.0.8", + "zod": "^3.25.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^6.0.1", + "vite-plugin-singlefile": "^2.3.0" + } +} diff --git a/example-mcp-app/src/app/mcp-app.tsx b/example-mcp-app/src/app/mcp-app.tsx new file mode 100644 index 00000000..50bf6d54 --- /dev/null +++ b/example-mcp-app/src/app/mcp-app.tsx @@ -0,0 +1,274 @@ +// MCP App UI that renders inside a sandboxed iframe in Claude, ChatGPT, etc. +// Uses Leaflet for the map and @modelcontextprotocol/ext-apps for host communication. +import { StrictMode, useCallback, useEffect, useRef, useState } from 'react' +import { createRoot } from 'react-dom/client' +import { useApp } from '@modelcontextprotocol/ext-apps/react' +import type { + App, + McpUiHostContext, +} from '@modelcontextprotocol/ext-apps' +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js' +import { z } from 'zod' + +// Leaflet loaded from CDN (declared globally) +declare const L: typeof import('leaflet') + +const PREFERRED_HEIGHT = 400 + +async function loadLeaflet(): Promise { + if (typeof L !== 'undefined') return + return new Promise((resolve, reject) => { + const script = document.createElement('script') + script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js' + script.onload = () => resolve() + script.onerror = () => reject(new Error('Failed to load Leaflet')) + document.head.appendChild(script) + }) +} + +interface BoundingBox { + west: number + south: number + east: number + north: number +} + +function extractText(result: CallToolResult): string { + return result.content?.find((c) => c.type === 'text')?.text ?? '' +} + +// --------------------------------------------------------------------------- +// Map component +// --------------------------------------------------------------------------- + +function MapView({ + app, + initialBbox, + label, +}: { + app: App + initialBbox: BoundingBox | null + label?: string +}) { + const containerRef = useRef(null) + const mapRef = useRef(null) + + // Initialize the map once + useEffect(() => { + if (!containerRef.current || mapRef.current) return + + const map = L.map(containerRef.current, { + zoomControl: true, + attributionControl: true, + }) + + L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, + attribution: + '© OpenStreetMap', + }).addTo(map) + + // Default view: world + map.setView([20, 0], 2) + mapRef.current = map + + // Let the host know our preferred size + app.sendSizeChanged({ height: PREFERRED_HEIGHT }) + + // Camera move updates model context + map.on('moveend', () => { + const center = map.getCenter() + const bounds = map.getBounds() + app.updateModelContext({ + content: [ + { + type: 'text', + text: `Map centered on [${center.lat.toFixed(4)}, ${center.lng.toFixed(4)}], zoom ${map.getZoom()}`, + }, + ], + }) + }) + + return () => { + map.remove() + mapRef.current = null + } + }, [app]) + + // Fly to initial bbox when it arrives + useEffect(() => { + if (!initialBbox || !mapRef.current) return + const { south, west, north, east } = initialBbox + mapRef.current.fitBounds([ + [south, west], + [north, east], + ]) + }, [initialBbox]) + + // Show label as a popup at center + useEffect(() => { + if (!label || !mapRef.current || !initialBbox) return + const lat = (initialBbox.south + initialBbox.north) / 2 + const lng = (initialBbox.west + initialBbox.east) / 2 + L.popup() + .setLatLng([lat, lng]) + .setContent(label) + .openOn(mapRef.current) + }, [label, initialBbox]) + + return ( +
+ ) +} + +// --------------------------------------------------------------------------- +// Main App +// --------------------------------------------------------------------------- + +function McpMapApp() { + const [bbox, setBbox] = useState(null) + const [label, setLabel] = useState() + const [hostContext, setHostContext] = useState() + const [leafletReady, setLeafletReady] = useState(false) + const appRef = useRef(null) + + const { app, error } = useApp({ + appInfo: { name: 'Spiceflow Map', version: '1.0.0' }, + capabilities: { tools: { listChanged: true } }, + autoResize: false, + onAppCreated: (app) => { + appRef.current = app + + app.ontoolinput = (params) => { + const args = params.arguments as Record | undefined + if (!args) return + if ( + args.west !== undefined && + args.south !== undefined && + args.east !== undefined && + args.north !== undefined + ) { + setBbox({ + west: args.west as number, + south: args.south as number, + east: args.east as number, + north: args.north as number, + }) + if (args.label) setLabel(args.label as string) + } + } + + app.ontoolresult = (result) => { + console.log('[mcp-app] tool result:', result) + } + + app.ontoolcancelled = (params) => { + console.log('[mcp-app] cancelled:', params.reason) + } + + app.onteardown = async () => { + console.log('[mcp-app] teardown') + return {} + } + + app.onerror = console.error + + app.onhostcontextchanged = (ctx) => { + setHostContext((prev) => ({ ...prev, ...ctx })) + } + + // Register a tool the LLM can call to navigate the map + app.registerTool( + 'navigate-to', + { + title: 'Navigate To', + description: 'Pan the map to a new bounding box', + inputSchema: z.object({ + west: z.number().describe('Western longitude'), + south: z.number().describe('Southern latitude'), + east: z.number().describe('Eastern longitude'), + north: z.number().describe('Northern latitude'), + label: z.string().optional().describe('Label to show'), + }), + }, + async (args) => { + setBbox({ + west: args.west, + south: args.south, + east: args.east, + north: args.north, + }) + if (args.label) setLabel(args.label) + return { + content: [ + { + type: 'text' as const, + text: `Navigated to [${args.south},${args.west}]-[${args.north},${args.east}]`, + }, + ], + } + }, + ) + }, + }) + + // Load Leaflet from CDN + useEffect(() => { + loadLeaflet().then(() => setLeafletReady(true)) + }, []) + + useEffect(() => { + if (app) setHostContext(app.getHostContext()) + }, [app]) + + const handleGeocode = useCallback( + async (query: string) => { + if (!app) return + const result = await app.callServerTool({ + name: 'geocode', + arguments: { query }, + }) + console.log('[mcp-app] geocode result:', extractText(result)) + }, + [app], + ) + + if (error) { + return ( +
+ Connection error: {error.message} +
+ ) + } + + if (!app || !leafletReady) { + return ( +
+ Loading map... +
+ ) + } + + return +} + +// --------------------------------------------------------------------------- +// Mount +// --------------------------------------------------------------------------- + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/example-mcp-app/src/globals.css b/example-mcp-app/src/globals.css new file mode 100644 index 00000000..d4b50785 --- /dev/null +++ b/example-mcp-app/src/globals.css @@ -0,0 +1 @@ +@import 'tailwindcss'; diff --git a/example-mcp-app/src/main.tsx b/example-mcp-app/src/main.tsx new file mode 100644 index 00000000..3a14dc98 --- /dev/null +++ b/example-mcp-app/src/main.tsx @@ -0,0 +1,201 @@ +// Spiceflow app that serves both a website and an MCP server endpoint. +// The website is a normal Spiceflow RSC app with pages. +// The /mcp endpoint handles MCP protocol requests (tools, resources). +import './globals.css' +import { Spiceflow } from 'spiceflow' +import { Head, Link, ProgressBar } from 'spiceflow/react' +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' +import { createMcpServer } from './mcp-server.js' + +export const app = new Spiceflow() + .layout('/*', async ({ children }) => { + return ( + + + Spiceflow MCP App + + + +
+ {children} +
+ + + ) + }) + .page('/', async function Home() { + return ( +
+

Spiceflow MCP App

+

+ This app is both a website (what you see here) and an{' '} + MCP server that Claude, ChatGPT, and other LLM hosts + can connect to. +

+ +
+

MCP tools

+
    +
  • + geocode — search for places by name +
  • +
  • + show-map — display an interactive Leaflet map in the + chat +
  • +
+
+ +
+

Connect to this server

+

+ Add as a custom connector with endpoint: +

+ + http://localhost:3000/mcp + +
+ + + About this example + +
+ ) + }) + .page('/about', async function About() { + return ( +
+

About

+

+ This example shows how a single Spiceflow app can serve a website with + React Server Components and act as an MCP server with + interactive UI apps that render inline in LLM chat clients. +

+ + Back home + +
+ ) + }) + // MCP endpoint: handles the Model Context Protocol over HTTP + .post('/mcp', async ({ request }) => { + const server = createMcpServer() + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }) + + // We need to adapt from Web Request/Response to the Node.js-style + // req/res that StreamableHTTPServerTransport.handleRequest expects. + const body = await request.json() + + // Create a minimal Node.js-compatible response wrapper + const { readable, writable } = new TransformStream() + const writer = writable.getWriter() + const headers: Record = {} + let statusCode = 200 + let headersSent = false + + const fakeRes = { + statusCode, + headersSent: false, + setHeader(name: string, value: string) { + headers[name.toLowerCase()] = value + }, + writeHead(code: number, hdrs?: Record) { + statusCode = code + if (hdrs) { + for (const [k, v] of Object.entries(hdrs)) { + headers[k.toLowerCase()] = v + } + } + headersSent = true + fakeRes.headersSent = true + return fakeRes + }, + write(chunk: string | Buffer) { + const data = + typeof chunk === 'string' + ? new TextEncoder().encode(chunk) + : new Uint8Array(chunk) + writer.write(data) + return true + }, + end(chunk?: string | Buffer) { + if (chunk) fakeRes.write(chunk) + writer.close() + }, + on(_event: string, _handler: Function) { + return fakeRes + }, + once(_event: string, _handler: Function) { + return fakeRes + }, + emit(_event: string, ..._args: unknown[]) { + return true + }, + removeListener(_event: string, _handler: Function) { + return fakeRes + }, + get writableEnded() { + return false + }, + get writableFinished() { + return false + }, + // Support status() for express-like usage + status(code: number) { + statusCode = code + return fakeRes + }, + json(data: unknown) { + headers['content-type'] = 'application/json' + fakeRes.end(JSON.stringify(data)) + }, + } + + const fakeReq = { + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + url: new URL(request.url).pathname, + body, + } + + try { + await server.connect(transport) + await transport.handleRequest(fakeReq as any, fakeRes as any, body) + } catch (error) { + console.error('MCP error:', error) + if (!headersSent) { + return Response.json( + { + jsonrpc: '2.0', + error: { code: -32603, message: 'Internal server error' }, + id: null, + }, + { status: 500 }, + ) + } + } + + return new Response(readable, { + status: statusCode, + headers, + }) + }) + +const port = Number(process.env.PORT || 3000) +void app.listen(port) +console.log(`Spiceflow MCP App listening on http://localhost:${port}`) +console.log(`MCP endpoint: http://localhost:${port}/mcp`) + +declare module 'spiceflow/react' { + interface SpiceflowRegister { + app: typeof app + } +} diff --git a/example-mcp-app/src/mcp-server.ts b/example-mcp-app/src/mcp-server.ts new file mode 100644 index 00000000..73a08704 --- /dev/null +++ b/example-mcp-app/src/mcp-server.ts @@ -0,0 +1,184 @@ +// MCP server factory. Creates a new McpServer instance with tools and resources +// registered. Each HTTP request gets its own instance because McpServer only +// supports one transport at a time. +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import type { + CallToolResult, + ReadResourceResult, +} from '@modelcontextprotocol/sdk/types.js' +import { + registerAppTool, + registerAppResource, + RESOURCE_MIME_TYPE, +} from '@modelcontextprotocol/ext-apps/server' +import { z } from 'zod' +import fs from 'node:fs/promises' +import path from 'node:path' +import { randomUUID } from 'node:crypto' + +interface NominatimResult { + place_id: number + lat: string + lon: string + display_name: string + boundingbox: [string, string, string, string] + type: string + importance: number +} + +let lastNominatimRequest = 0 +const NOMINATIM_RATE_LIMIT_MS = 1100 + +async function geocode(query: string): Promise { + const now = Date.now() + const wait = NOMINATIM_RATE_LIMIT_MS - (now - lastNominatimRequest) + if (wait > 0) await new Promise((r) => setTimeout(r, wait)) + lastNominatimRequest = Date.now() + + const params = new URLSearchParams({ q: query, format: 'json', limit: '5' }) + const res = await fetch( + `https://nominatim.openstreetmap.org/search?${params}`, + { + headers: { + 'User-Agent': 'Spiceflow-MCP-Example/1.0 (https://github.com/remorses/spiceflow)', + }, + }, + ) + if (!res.ok) throw new Error(`Nominatim ${res.status} ${res.statusText}`) + return res.json() as Promise +} + +// Resolve the dist directory for the pre-built MCP App UI HTML. +// In dev we read from dist-mcp-ui/ (built by `pnpm build:mcp-ui`). +function getMcpUiDistDir(): string { + return path.join(import.meta.dirname, '..', 'dist-mcp-ui') +} + +const RESOURCE_URI = 'ui://spiceflow-map/mcp-app.html' + +export function createMcpServer(): McpServer { + const server = new McpServer({ + name: 'Spiceflow Map Server', + version: '1.0.0', + }) + + const cspMeta = { + ui: { + csp: { + connectDomains: ['https://*.openstreetmap.org'], + resourceDomains: [ + 'https://*.openstreetmap.org', + 'https://unpkg.com', + ], + }, + }, + } + + // Serve the bundled single-file HTML as an MCP resource + registerAppResource( + server, + RESOURCE_URI, + RESOURCE_URI, + { mimeType: RESOURCE_MIME_TYPE }, + async (): Promise => { + const html = await fs.readFile( + path.join(getMcpUiDistDir(), 'mcp-app.html'), + 'utf-8', + ) + return { + contents: [ + { + uri: RESOURCE_URI, + mimeType: RESOURCE_MIME_TYPE, + text: html, + _meta: cspMeta, + }, + ], + } + }, + ) + + // show-map tool: displays the interactive Leaflet map + registerAppTool( + server, + 'show-map', + { + title: 'Show Map', + description: + 'Display an interactive map zoomed to a bounding box. Use the geocode tool first to find coordinates.', + inputSchema: { + west: z.number().optional().default(-0.5).describe('Western longitude'), + south: z.number().optional().default(51.3).describe('Southern latitude'), + east: z.number().optional().default(0.3).describe('Eastern longitude'), + north: z.number().optional().default(51.7).describe('Northern latitude'), + label: z.string().optional().describe('Label to display on the map'), + }, + _meta: { ui: { resourceUri: RESOURCE_URI } }, + }, + async ({ west, south, east, north, label }): Promise => ({ + content: [ + { + type: 'text', + text: `Map: W:${west.toFixed(4)}, S:${south.toFixed(4)}, E:${east.toFixed(4)}, N:${north.toFixed(4)}${label ? ` (${label})` : ''}`, + }, + ], + _meta: { viewUUID: randomUUID() }, + }), + ) + + // geocode tool: search for places (no UI) + server.registerTool( + 'geocode', + { + title: 'Geocode', + description: + 'Search for places using OpenStreetMap. Returns coordinates and bounding boxes.', + inputSchema: { + query: z.string().describe('Place name or address to search for'), + }, + }, + async ({ query }): Promise => { + try { + const results = await geocode(query) + if (results.length === 0) { + return { + content: [{ type: 'text', text: `No results for "${query}"` }], + } + } + + const formatted = results.map((r) => ({ + name: r.display_name, + lat: parseFloat(r.lat), + lon: parseFloat(r.lon), + bbox: { + south: parseFloat(r.boundingbox[0]), + north: parseFloat(r.boundingbox[1]), + west: parseFloat(r.boundingbox[2]), + east: parseFloat(r.boundingbox[3]), + }, + })) + + const text = formatted + .map( + (r, i) => + `${i + 1}. ${r.name}\n [${r.lat.toFixed(6)}, ${r.lon.toFixed(6)}]\n bbox: W:${r.bbox.west.toFixed(4)} S:${r.bbox.south.toFixed(4)} E:${r.bbox.east.toFixed(4)} N:${r.bbox.north.toFixed(4)}`, + ) + .join('\n\n') + + return { content: [{ type: 'text', text }] } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Geocoding error: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + } + } + }, + ) + + return server +} diff --git a/example-mcp-app/tsconfig.json b/example-mcp-app/tsconfig.json new file mode 100644 index 00000000..49bc0532 --- /dev/null +++ b/example-mcp-app/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["src", "*.ts"] +} diff --git a/example-mcp-app/vite.config.ts b/example-mcp-app/vite.config.ts new file mode 100644 index 00000000..5273d3e6 --- /dev/null +++ b/example-mcp-app/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import spiceflow from 'spiceflow/vite' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + clearScreen: false, + plugins: [ + spiceflow({ + entry: './src/main.tsx', + }), + react(), + tailwindcss(), + ], +}) diff --git a/example-mcp-app/vite.mcp-ui.config.ts b/example-mcp-app/vite.mcp-ui.config.ts new file mode 100644 index 00000000..03ef9557 --- /dev/null +++ b/example-mcp-app/vite.mcp-ui.config.ts @@ -0,0 +1,21 @@ +// Separate Vite config for building the MCP App UI into a single HTML file. +// MCP Apps render inside sandboxed iframes via srcdoc, so all JS/CSS must be +// inlined into one self-contained HTML blob. vite-plugin-singlefile handles this. +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { viteSingleFile } from 'vite-plugin-singlefile' + +const INPUT = process.env.INPUT +if (!INPUT) { + throw new Error('INPUT environment variable is not set') +} + +export default defineConfig({ + plugins: [react(), viteSingleFile()], + build: { + outDir: 'dist-mcp-ui', + rollupOptions: { + input: INPUT, + }, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f1777d1..966d4c0f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -221,6 +221,52 @@ importers: specifier: ^6.0.1 version: 6.0.1(vite@8.0.8(@types/node@24.0.15)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)(tsx@4.20.6)(yaml@2.7.0)) + example-mcp-app: + dependencies: + '@modelcontextprotocol/ext-apps': + specifier: ^1.7.0 + version: 1.7.0(@modelcontextprotocol/sdk@1.29.0(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76) + '@modelcontextprotocol/sdk': + specifier: ^1.29.0 + version: 1.29.0(zod@3.25.76) + '@tailwindcss/vite': + specifier: ^4.2.2 + version: 4.2.2(vite@8.0.8(@types/node@24.0.15)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)(tsx@4.20.6)(yaml@2.7.0)) + '@types/react': + specifier: 19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.14) + react: + specifier: 19.2.4 + version: 19.2.4 + react-dom: + specifier: 19.2.4 + version: 19.2.4(react@19.2.4) + spiceflow: + specifier: workspace:^ + version: link:../spiceflow + tailwindcss: + specifier: 4.0.6 + version: 4.0.6 + typescript: + specifier: 5.7.3 + version: 5.7.3 + vite: + specifier: ^8.0.8 + version: 8.0.8(@types/node@24.0.15)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)(tsx@4.20.6)(yaml@2.7.0) + zod: + specifier: ^3.25.0 + version: 3.25.76 + devDependencies: + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.8(@types/node@24.0.15)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)(tsx@4.20.6)(yaml@2.7.0)) + vite-plugin-singlefile: + specifier: ^2.3.0 + version: 2.3.3(vite@8.0.8(@types/node@24.0.15)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)(tsx@4.20.6)(yaml@2.7.0)) + example-nodejs: dependencies: '@tailwindcss/vite': @@ -1808,6 +1854,20 @@ packages: peerDependencies: rollup: '>=2' + '@modelcontextprotocol/ext-apps@1.7.0': + resolution: {integrity: sha512-gs8rYVx6a8pyCvSpXq7TyVLTERCC94JLrcmJgBs0+3p4jp3iQdJPu1IU+2ovVdFZ1sW8JgmvTkRnxAlIizKINg==} + engines: {node: '>=20'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.29.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + '@modelcontextprotocol/sdk@1.27.1': resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} engines: {node: '>=18'} @@ -1818,6 +1878,16 @@ packages: '@cfworker/json-schema': optional: true + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@napi-rs/wasm-runtime@1.1.3': resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} peerDependencies: @@ -6598,6 +6668,16 @@ packages: '@nuxt/kit': optional: true + vite-plugin-singlefile@2.3.3: + resolution: {integrity: sha512-XVnGH0QzbOa8fxRSsHdCarVN1BSBXNi7uLMQYlrGRN5apdHkk62XQWRJhVever0lnfuyBkwn+kvVChdm/OoOUg==} + engines: {node: '>18.0.0'} + peerDependencies: + rollup: ^4.59.0 + vite: ^5.4.21 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + rollup: + optional: true + vite@8.0.8: resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -7974,6 +8054,15 @@ snapshots: - acorn - supports-color + '@modelcontextprotocol/ext-apps@1.7.0(@modelcontextprotocol/sdk@1.29.0(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76)': + dependencies: + '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) + '@standard-schema/spec': 1.1.0 + zod: 3.25.76 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + '@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)': dependencies: '@hono/node-server': 1.19.11(hono@4.12.8) @@ -7996,6 +8085,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.11(hono@4.12.8) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.3 + express: 5.2.1 + express-rate-limit: 8.3.0(express@5.2.1) + hono: 4.12.8 + jose: 6.2.0 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.0 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: '@emnapi/core': 1.9.2 @@ -13894,6 +14005,11 @@ snapshots: transitivePeerDependencies: - supports-color + vite-plugin-singlefile@2.3.3(vite@8.0.8(@types/node@24.0.15)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)(tsx@4.20.6)(yaml@2.7.0)): + dependencies: + micromatch: 4.0.8 + vite: 8.0.8(@types/node@24.0.15)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)(tsx@4.20.6)(yaml@2.7.0) + vite@8.0.8(@types/node@18.16.3)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)(tsx@4.20.6)(yaml@2.7.0): dependencies: lightningcss: 1.32.0 @@ -14230,6 +14346,10 @@ snapshots: zimmerframe@1.1.4: {} + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod-to-json-schema@3.25.1(zod@4.3.6): dependencies: zod: 4.3.6