From 697a7ac658dc5f3b4f809164742c12ab3d7de287 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 21 Nov 2025 12:59:41 +0000 Subject: [PATCH] Add standalone Vite build for simple-react example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace custom build.ts dependency with standard Vite setup: - package.json with Vite scripts (dev, build, preview) - Use @modelcontextprotocol/ext-apps package imports - Standard Vite project structure (index.html at root) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- examples/simple-react/index.html | 12 ++ examples/simple-react/package.json | 24 +++ .../simple-react/src/example-ui-react.tsx | 194 ++++++++++++++++++ examples/simple-react/tsconfig.json | 20 ++ examples/simple-react/vite.config.ts | 14 ++ 5 files changed, 264 insertions(+) create mode 100644 examples/simple-react/index.html create mode 100644 examples/simple-react/package.json create mode 100644 examples/simple-react/src/example-ui-react.tsx create mode 100644 examples/simple-react/tsconfig.json create mode 100644 examples/simple-react/vite.config.ts diff --git a/examples/simple-react/index.html b/examples/simple-react/index.html new file mode 100644 index 0000000..33b0e50 --- /dev/null +++ b/examples/simple-react/index.html @@ -0,0 +1,12 @@ + + + + + + MCP UI Client (React) + + +
+ + + diff --git a/examples/simple-react/package.json b/examples/simple-react/package.json new file mode 100644 index 0000000..bd360d5 --- /dev/null +++ b/examples/simple-react/package.json @@ -0,0 +1,24 @@ +{ + "name": "simple-app-react", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@modelcontextprotocol/ext-apps": "file:../..", + "@modelcontextprotocol/sdk": "^1.20.1", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.7.2", + "vite": "^6.0.0" + } +} diff --git a/examples/simple-react/src/example-ui-react.tsx b/examples/simple-react/src/example-ui-react.tsx new file mode 100644 index 0000000..687a580 --- /dev/null +++ b/examples/simple-react/src/example-ui-react.tsx @@ -0,0 +1,194 @@ +import { useState, useCallback } from "react"; +import { createRoot } from "react-dom/client"; +import { useApp } from "@modelcontextprotocol/ext-apps/react/useApp"; +import type { + CallToolResult, + Implementation, +} from "@modelcontextprotocol/sdk/types.js"; +import { + McpUiSizeChangeNotificationSchema, + McpUiToolResultNotificationSchema, +} from "@modelcontextprotocol/ext-apps/types"; + +const APP_INFO: Implementation = { + name: "MCP UI React Example Client", + version: "1.0.0", + protocolVersion: "2025-06-18", +}; +export function McpClientApp() { + const [toolResults, setToolResults] = useState([]); + const [messages, setMessages] = useState([]); + + const { app, isConnected, error } = useApp({ + appInfo: APP_INFO, + capabilities: {}, + onAppCreated: (app) => { + app.setNotificationHandler( + McpUiToolResultNotificationSchema, + async (notification) => { + setToolResults((prev) => [...prev, notification.params]); + }, + ); + app.setNotificationHandler( + McpUiSizeChangeNotificationSchema, + async (notification) => { + document.body.style.width = `${notification.params.width}px`; + document.body.style.height = `${notification.params.height}px`; + }, + ); + }, + }); + + const handleGetWeather = useCallback(async () => { + if (!app) return; + try { + const result = await app.callServerTool({ + name: "get-weather", + arguments: { city: "Tokyo" }, + }); + setMessages((prev) => [ + ...prev, + `Weather tool result: ${JSON.stringify(result)}`, + ]); + } catch (e) { + setMessages((prev) => [...prev, `Tool call error: ${e}`]); + } + }, [app]); + + const handleNotifyCart = useCallback(async () => { + if (!app) return; + await app.sendLog({ level: "info", data: "cart-updated" }); + setMessages((prev) => [...prev, "Notification sent: cart-updated"]); + }, [app]); + + const handlePromptWeather = useCallback(async () => { + if (!app) return; + const signal = AbortSignal.timeout(5000); + try { + const { isError } = await app.sendMessage( + { + role: "user", + content: [ + { + type: "text", + text: "What is the weather in Tokyo?", + }, + ], + }, + { signal }, + ); + setMessages((prev) => [ + ...prev, + `Prompt result: ${isError ? "error" : "success"}`, + ]); + } catch (e) { + if (signal.aborted) { + setMessages((prev) => [...prev, "Prompt request timed out"]); + return; + } + setMessages((prev) => [...prev, `Prompt error: ${e}`]); + } + }, [app]); + + const handleOpenLink = useCallback(async () => { + if (!app) return; + const { isError } = await app.sendOpenLink({ + url: "https://www.google.com", + }); + setMessages((prev) => [ + ...prev, + `Open link result: ${isError ? "error" : "success"}`, + ]); + }, [app]); + + if (error) { + return ( +
Error connecting: {error.message}
+ ); + } + + if (!isConnected) { + return
Connecting...
; + } + + return ( +
+

MCP UI Client (React)

+ +
+ + + + + + + +
+ + {toolResults.length > 0 && ( +
+

Tool Results:

+ {toolResults.map((result, i) => ( +
+ isError: {String(result.isError ?? false)} +
+ content: {JSON.stringify(result.content)} +
+ {result.structuredContent && ( + <> + structuredContent:{" "} + {JSON.stringify(result.structuredContent)} + + )} +
+ ))} +
+ )} + + {messages.length > 0 && ( +
+

Messages:

+ {messages.map((msg, i) => ( +
+ {msg} +
+ ))} +
+ )} +
+ ); +} + +window.addEventListener("load", () => { + const root = document.getElementById("root"); + if (!root) { + throw new Error("Root element not found"); + } + + createRoot(root).render(); +}); diff --git a/examples/simple-react/tsconfig.json b/examples/simple-react/tsconfig.json new file mode 100644 index 0000000..a4c834a --- /dev/null +++ b/examples/simple-react/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/simple-react/vite.config.ts b/examples/simple-react/vite.config.ts new file mode 100644 index 0000000..8215d23 --- /dev/null +++ b/examples/simple-react/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + build: { + // Output single HTML file with inlined JS for easy distribution + rollupOptions: { + output: { + inlineDynamicImports: true, + }, + }, + }, +});