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