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 (
-
+
+
+
+ );
+}
+
+
+// ServerSelect calls use() and renders the server