From 8bff1dff68e20cbeb4402ae04710aa93812c0939 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 8 Dec 2025 13:56:10 -0600 Subject: [PATCH 1/2] Add dynamic example orchestration script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hardcoded port assignments and npm scripts with run-all.ts that: - Auto-discovers example directories and assigns ports - Passes server URLs to basic-host via SERVERS env var - Supports start, dev, and build commands Update basic-host to fetch server list from /api/servers endpoint and display server names from MCP server metadata. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- examples/basic-host/serve.ts | 10 +- examples/basic-host/src/implementation.ts | 5 +- examples/basic-host/src/index.tsx | 106 ++++++++----------- examples/basic-server-react/server.ts | 4 +- examples/basic-server-vanillajs/server.ts | 4 +- examples/run-all.ts | 120 ++++++++++++++++++++++ package.json | 18 +--- 7 files changed, 180 insertions(+), 87 deletions(-) create mode 100644 examples/run-all.ts diff --git a/examples/basic-host/serve.ts b/examples/basic-host/serve.ts index 450ec5712..1b9f3cc20 100644 --- a/examples/basic-host/serve.ts +++ b/examples/basic-host/serve.ts @@ -18,6 +18,7 @@ const __dirname = dirname(__filename); const HOST_PORT = parseInt(process.env.HOST_PORT || "8080", 10); const SANDBOX_PORT = parseInt(process.env.SANDBOX_PORT || "8081", 10); const DIRECTORY = join(__dirname, "dist"); +const SERVERS: string[] = process.env.SERVERS ? JSON.parse(process.env.SERVERS) : []; // ============ Host Server (port 8080) ============ const hostApp = express(); @@ -34,6 +35,11 @@ hostApp.use((req, res, next) => { hostApp.use(express.static(DIRECTORY)); +// API endpoint to get configured server URLs +hostApp.get("/api/servers", (_req, res) => { + res.json(SERVERS); +}); + hostApp.get("/", (_req, res) => { res.redirect("/index.html"); }); @@ -70,7 +76,7 @@ sandboxApp.use((_req, res) => { }); // ============ Start both servers ============ -hostApp.listen(HOST_PORT, err => { +hostApp.listen(HOST_PORT, (err) => { if (err) { console.error("Error starting server:", err); process.exit(1); @@ -78,7 +84,7 @@ hostApp.listen(HOST_PORT, err => { console.log(`Host server: http://localhost:${HOST_PORT}`); }); -sandboxApp.listen(SANDBOX_PORT, err => { +sandboxApp.listen(SANDBOX_PORT, (err) => { if (err) { console.error("Error starting server:", err); process.exit(1); diff --git a/examples/basic-host/src/implementation.ts b/examples/basic-host/src/implementation.ts index 614aeead4..3fc846377 100644 --- a/examples/basic-host/src/implementation.ts +++ b/examples/basic-host/src/implementation.ts @@ -17,6 +17,7 @@ export const log = { export interface ServerInfo { + name: string; client: Client; tools: Map; appHtmlCache: Map; @@ -30,11 +31,13 @@ export async function connectToServer(serverUrl: URL): Promise { await client.connect(new StreamableHTTPClientTransport(serverUrl)); log.info("Connection successful"); + const name = client.getServerVersion()?.name ?? serverUrl.href; + const toolsList = await client.listTools(); const tools = new Map(toolsList.tools.map((tool) => [tool.name, tool])); log.info("Server tools:", Array.from(tools.keys())); - return { client, tools, appHtmlCache: new Map() }; + return { name, client, tools, appHtmlCache: new Map() }; } diff --git a/examples/basic-host/src/index.tsx b/examples/basic-host/src/index.tsx index 5a346fc71..0307d12cb 100644 --- a/examples/basic-host/src/index.tsx +++ b/examples/basic-host/src/index.tsx @@ -4,53 +4,31 @@ import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy, import styles from "./index.module.css"; -// Available MCP servers - using ports 3101+ to avoid conflicts with common dev ports -const SERVERS = [ - { name: "Basic React", port: 3101 }, - { name: "Vanilla JS", port: 3102 }, - { name: "Budget Allocator", port: 3103 }, - { name: "Cohort Heatmap", port: 3104 }, - { name: "Customer Segmentation", port: 3105 }, - { name: "Scenario Modeler", port: 3106 }, - { name: "System Monitor", port: 3107 }, - { name: "Three.js", port: 3109 }, -] as const; - -function serverUrl(port: number): string { - return `http://localhost:${port}/mcp`; -} - -// Cache server connections to avoid reconnecting when switching between servers -const serverInfoCache = new Map>(); - -function getServerInfo(port: number): Promise { - let promise = serverInfoCache.get(port); - if (!promise) { - promise = connectToServer(new URL(serverUrl(port))); - // Remove from cache on failure so retry is possible - promise.catch(() => serverInfoCache.delete(port)); - serverInfoCache.set(port, promise); - } - return promise; -} - - // Wrapper to track server name with each tool call interface ToolCallEntry { serverName: string; info: ToolCallInfo; } -// Host just manages tool call results - no server dependency -function Host() { +// Host receives connected servers via promise, uses single use() call +interface HostProps { + serversPromise: Promise; +} +function Host({ serversPromise }: HostProps) { + const servers = use(serversPromise); const [toolCalls, setToolCalls] = useState([]); + if (servers.length === 0) { + return

No servers configured. Set SERVERS environment variable.

; + } + return ( <> {toolCalls.map((entry, i) => ( ))} setToolCalls([...toolCalls, { serverName, info }])} /> @@ -58,60 +36,48 @@ function Host() { } -// CallToolPanel includes server selection with its own Suspense boundary +// CallToolPanel manages server selection from already-connected servers interface CallToolPanelProps { + servers: ServerInfo[]; addToolCall: (serverName: string, info: ToolCallInfo) => void; } -function CallToolPanel({ addToolCall }: CallToolPanelProps) { - const [selectedServer, setSelectedServer] = useState(SERVERS[0]); - const [serverInfoPromise, setServerInfoPromise] = useState( - () => getServerInfo(selectedServer.port) - ); - - const handleServerChange = (port: number) => { - const server = SERVERS.find(s => s.port === port) ?? SERVERS[0]; - setSelectedServer(server); - setServerInfoPromise(getServerInfo(port)); - }; +function CallToolPanel({ servers, addToolCall }: CallToolPanelProps) { + const [selectedIndex, setSelectedIndex] = useState(0); + const selectedServer = servers[selectedIndex]; return (
- - Connecting to {serverUrl(selectedServer.port)}...

}> - -
-
+
); } -// ToolCallForm renders inside Suspense - needs serverInfo for tool list +// ToolCallForm receives already-resolved serverInfo interface ToolCallFormProps { serverName: string; - serverInfoPromise: Promise; + serverInfo: ServerInfo; addToolCall: (serverName: string, info: ToolCallInfo) => void; } -function ToolCallForm({ serverName, serverInfoPromise, addToolCall }: ToolCallFormProps) { - const serverInfo = use(serverInfoPromise); +function ToolCallForm({ serverName, serverInfo, addToolCall }: ToolCallFormProps) { const toolNames = Array.from(serverInfo.tools.keys()); const [selectedTool, setSelectedTool] = useState(toolNames[0] ?? ""); const [inputJson, setInputJson] = useState("{}"); @@ -264,8 +230,18 @@ class ErrorBoundary extends Component { } +async function connectToAllServers(): Promise { + const serverUrlsResponse = await fetch("/api/servers"); + const serverUrls = (await serverUrlsResponse.json()) as string[]; + return Promise.all(serverUrls.map((url) => connectToServer(new URL(url)))); +} + createRoot(document.getElementById("root")!).render( - + + Connecting to servers...

}> + +
+
, ); diff --git a/examples/basic-server-react/server.ts b/examples/basic-server-react/server.ts index eff3120d1..47706330d 100644 --- a/examples/basic-server-react/server.ts +++ b/examples/basic-server-react/server.ts @@ -12,7 +12,7 @@ const DIST_DIR = path.join(import.meta.dirname, "dist"); const server = new McpServer({ - name: "MCP App Server", + name: "Basic MCP App Server (React-based)", version: "1.0.0", }); @@ -85,7 +85,7 @@ app.post("/mcp", async (req: Request, res: Response) => { } }); -const httpServer = app.listen(PORT, err => { +const httpServer = app.listen(PORT, (err) => { if (err) { console.error("Error starting server:", err); process.exit(1); diff --git a/examples/basic-server-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts index eff3120d1..d42cac286 100644 --- a/examples/basic-server-vanillajs/server.ts +++ b/examples/basic-server-vanillajs/server.ts @@ -12,7 +12,7 @@ const DIST_DIR = path.join(import.meta.dirname, "dist"); const server = new McpServer({ - name: "MCP App Server", + name: "Basic MCP App Server (Vanilla JS)", version: "1.0.0", }); @@ -85,7 +85,7 @@ app.post("/mcp", async (req: Request, res: Response) => { } }); -const httpServer = app.listen(PORT, err => { +const httpServer = app.listen(PORT, (err) => { if (err) { console.error("Error starting server:", err); process.exit(1); diff --git a/examples/run-all.ts b/examples/run-all.ts new file mode 100644 index 000000000..e913afd78 --- /dev/null +++ b/examples/run-all.ts @@ -0,0 +1,120 @@ +#!/usr/bin/env bun +/** + * Orchestration script for running all example servers. + * + * Usage: + * bun examples/run-all.ts start - Build and start all examples + * bun examples/run-all.ts dev - Run all examples in dev/watch mode + * bun examples/run-all.ts build - Build all examples + */ + +import { readdirSync, statSync, existsSync } from "fs"; +import { spawn, type ChildProcess } from "child_process"; + +const BASE_PORT = 3101; +const BASIC_HOST = "basic-host"; + +// Find all example directories except basic-host that have a package.json, +// assign ports, and build URL list +const servers = readdirSync("examples") + .filter( + (d) => + d !== BASIC_HOST && + statSync(`examples/${d}`).isDirectory() && + existsSync(`examples/${d}/package.json`), + ) + .sort() // Sort for consistent port assignment + .map((dir, i) => ({ + dir, + port: BASE_PORT + i, + url: `http://localhost:${BASE_PORT + i}/mcp`, + })); + +const COMMANDS = ["start", "dev", "build"]; + +const command = process.argv[2]; + +if (!command || !COMMANDS.includes(command)) { + console.error(`Usage: bun examples/run-all.ts <${COMMANDS.join("|")}>`); + + process.exit(1); +} + +const processes: ChildProcess[] = []; + +// Handle cleanup on exit +function cleanup() { + for (const proc of processes) { + proc.kill(); + } +} +process.on("SIGINT", cleanup); +process.on("SIGTERM", cleanup); + +// Spawn a process and track it +function spawnProcess( + cmd: string, + args: string[], + env: Record = {}, + prefix: string, +): ChildProcess { + const proc = spawn(cmd, args, { + env: { ...process.env, ...env }, + stdio: ["ignore", "pipe", "pipe"], + }); + + proc.stdout?.on("data", (data) => { + const lines = data.toString().trim().split("\n"); + for (const line of lines) { + console.log(`[${prefix}] ${line}`); + } + }); + + proc.stderr?.on("data", (data) => { + const lines = data.toString().trim().split("\n"); + for (const line of lines) { + console.error(`[${prefix}] ${line}`); + } + }); + + proc.on("exit", (code) => { + if (code !== 0 && code !== null) { + console.error(`[${prefix}] exited with code ${code}`); + } + }); + + processes.push(proc); + return proc; +} + +// Build the SERVERS environment variable (JSON array of URLs) +const serversEnv = JSON.stringify(servers.map((s) => s.url)); + +console.log(`Running command: ${command}`); +console.log( + `Server examples: ${servers.map((s) => `${s.dir}:${s.port}`).join(", ")}`, +); +console.log(""); + +// If dev mode, also run the main library watcher +if (command === "dev") { + spawnProcess("npm", ["run", "watch"], {}, "lib"); +} + +// Run each server example +for (const { dir, port } of servers) { + spawnProcess( + "npm", + ["run", "--workspace", `examples/${dir}`, command], + { PORT: String(port) }, + dir, + ); +} + +// Run basic-host with the SERVERS env var +spawnProcess( + "npm", + ["run", "--workspace", `examples/${BASIC_HOST}`, command], + { SERVERS: serversEnv }, + BASIC_HOST, +); diff --git a/package.json b/package.json index ac8ea133d..85c6c87bc 100644 --- a/package.json +++ b/package.json @@ -31,22 +31,10 @@ "prepack": "npm run build", "build:all": "npm run build && npm run examples:build", "test": "bun test", - "examples:build": "find examples -maxdepth 1 -mindepth 1 -type d -exec printf '%s\\0' 'npm run --workspace={} build' ';' | xargs -0 concurrently --kill-others-on-fail", - "examples:start": "NODE_ENV=development npm run build && concurrently 'npm run examples:start:basic-host' 'npm run examples:start:basic-server-react' 'npm run examples:start:basic-server-vanillajs' 'npm run examples:start:budget-allocator-server' 'npm run examples:start:cohort-heatmap-server' 'npm run examples:start:customer-segmentation-server' 'npm run examples:start:scenario-modeler-server' 'npm run examples:start:system-monitor-server' 'npm run examples:start:threejs-server'", - "examples:start:basic-host": "npm run --workspace=examples/basic-host start", - "examples:start:basic-server-react": "PORT=3101 npm run --workspace=examples/basic-server-react start", - "examples:start:basic-server-vanillajs": "PORT=3102 npm run --workspace=examples/basic-server-vanillajs start", - "examples:start:budget-allocator-server": "PORT=3103 npm run --workspace=examples/budget-allocator-server start", - "examples:start:cohort-heatmap-server": "PORT=3104 npm run --workspace=examples/cohort-heatmap-server start", - "examples:start:customer-segmentation-server": "PORT=3105 npm run --workspace=examples/customer-segmentation-server start", - "examples:start:scenario-modeler-server": "PORT=3106 npm run --workspace=examples/scenario-modeler-server start", - "examples:start:system-monitor-server": "PORT=3107 npm run --workspace=examples/system-monitor-server start", - "examples:start:threejs-server": "PORT=3109 npm run --workspace=examples/threejs-server start", + "examples:build": "bun examples/run-all.ts build", + "examples:start": "NODE_ENV=development npm run build && bun examples/run-all.ts start", + "examples:dev": "NODE_ENV=development bun examples/run-all.ts dev", "watch": "nodemon --watch src --ext ts,tsx --exec 'bun build.bun.ts'", - "examples:dev": "NODE_ENV=development concurrently 'npm run watch' 'npm run examples:dev:basic-host' 'npm run examples:dev:basic-server-react'", - "examples:dev:basic-host": "npm run --workspace=examples/basic-host dev", - "examples:dev:basic-server-react": "npm run --workspace=examples/basic-server-react dev", - "examples:dev:basic-server-vanillajs": "npm run --workspace=examples/basic-server-vanillajs dev", "prepare": "npm run build && husky", "docs": "typedoc", "docs:watch": "typedoc --watch", From d11b656d8e0b47063212b1921acfdda5123a56a0 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 8 Dec 2025 15:20:32 -0600 Subject: [PATCH 2/2] Refactor basic-host to use ServerSelect component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract server dropdown into ServerSelect component and simplify ToolCallInfoPanel to read server name from toolCallInfo.serverInfo. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- examples/basic-host/src/index.module.css | 26 ++-- examples/basic-host/src/index.tsx | 190 ++++++++++++----------- 2 files changed, 112 insertions(+), 104 deletions(-) diff --git a/examples/basic-host/src/index.module.css b/examples/basic-host/src/index.module.css index 961d078ff..c162aa8c8 100644 --- a/examples/basic-host/src/index.module.css +++ b/examples/basic-host/src/index.module.css @@ -1,9 +1,3 @@ -.connecting { - padding: 1rem 0; - text-align: center; - color: #666; -} - .callToolPanel, .toolCallInfoPanel { margin: 0 auto; padding: 1rem; @@ -16,9 +10,6 @@ } .callToolPanel { - display: flex; - flex-direction: column; - gap: 1rem; max-width: 480px; form { @@ -39,12 +30,16 @@ padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; - font-family: monospace; font-size: inherit; } - textarea { + .toolSelect { + font-family: monospace; + } + + .toolInput { min-height: 6rem; + font-family: monospace; resize: vertical; &[aria-invalid="true"] { @@ -92,10 +87,15 @@ gap: 0.5rem; min-width: 0; - .toolName { + h2 { + display: flex; + flex-direction: column; margin: 0; - font-family: monospace; font-size: 1.5rem; + + .toolName { + font-family: monospace; + } } } diff --git a/examples/basic-host/src/index.tsx b/examples/basic-host/src/index.tsx index 0307d12cb..37eac43fc 100644 --- a/examples/basic-host/src/index.tsx +++ b/examples/basic-host/src/index.tsx @@ -4,84 +4,39 @@ import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy, import styles from "./index.module.css"; -// Wrapper to track server name with each tool call -interface ToolCallEntry { - serverName: string; - info: ToolCallInfo; -} - -// Host receives connected servers via promise, uses single use() call +// Host passes serversPromise to CallToolPanel interface HostProps { serversPromise: Promise; } function Host({ serversPromise }: HostProps) { - const servers = use(serversPromise); - const [toolCalls, setToolCalls] = useState([]); - - if (servers.length === 0) { - return

No servers configured. Set SERVERS environment variable.

; - } + const [toolCalls, setToolCalls] = useState([]); return ( <> - {toolCalls.map((entry, i) => ( - + {toolCalls.map((info, i) => ( + ))} setToolCalls([...toolCalls, { serverName, info }])} + serversPromise={serversPromise} + addToolCall={(info) => setToolCalls([...toolCalls, info])} /> ); } -// CallToolPanel manages server selection from already-connected servers +// CallToolPanel renders the unified form with Suspense around ServerSelect interface CallToolPanelProps { - servers: ServerInfo[]; - addToolCall: (serverName: string, info: ToolCallInfo) => void; -} -function CallToolPanel({ servers, addToolCall }: CallToolPanelProps) { - const [selectedIndex, setSelectedIndex] = useState(0); - const selectedServer = servers[selectedIndex]; - - return ( -
- - -
- ); -} - - -// ToolCallForm receives already-resolved serverInfo -interface ToolCallFormProps { - serverName: string; - serverInfo: ServerInfo; - addToolCall: (serverName: string, info: ToolCallInfo) => void; + serversPromise: Promise; + addToolCall: (info: ToolCallInfo) => void; } -function ToolCallForm({ serverName, serverInfo, addToolCall }: ToolCallFormProps) { - const toolNames = Array.from(serverInfo.tools.keys()); - const [selectedTool, setSelectedTool] = useState(toolNames[0] ?? ""); +function CallToolPanel({ serversPromise, addToolCall }: CallToolPanelProps) { + const [selectedServer, setSelectedServer] = useState(null); + const [selectedTool, setSelectedTool] = useState(""); const [inputJson, setInputJson] = useState("{}"); + const toolNames = selectedServer ? Array.from(selectedServer.tools.keys()) : []; + const isValidJson = useMemo(() => { try { JSON.parse(inputJson); @@ -91,49 +46,104 @@ function ToolCallForm({ serverName, serverInfo, addToolCall }: ToolCallFormProps } }, [inputJson]); + const handleServerSelect = (server: ServerInfo) => { + setSelectedServer(server); + const [firstTool] = server.tools.keys(); + setSelectedTool(firstTool ?? ""); + }; + const handleSubmit = () => { - const toolCallInfo = callTool(serverInfo, selectedTool, JSON.parse(inputJson)); - addToolCall(serverName, toolCallInfo); + if (!selectedServer) return; + const toolCallInfo = callTool(selectedServer, selectedTool, JSON.parse(inputJson)); + addToolCall(toolCallInfo); }; return ( -
{ e.preventDefault(); handleSubmit(); }}> - -