Skip to content

Commit 8bff1df

Browse files
Add dynamic example orchestration script
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 <[email protected]>
1 parent 2b53efd commit 8bff1df

File tree

7 files changed

+180
-87
lines changed

7 files changed

+180
-87
lines changed

examples/basic-host/serve.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const __dirname = dirname(__filename);
1818
const HOST_PORT = parseInt(process.env.HOST_PORT || "8080", 10);
1919
const SANDBOX_PORT = parseInt(process.env.SANDBOX_PORT || "8081", 10);
2020
const DIRECTORY = join(__dirname, "dist");
21+
const SERVERS: string[] = process.env.SERVERS ? JSON.parse(process.env.SERVERS) : [];
2122

2223
// ============ Host Server (port 8080) ============
2324
const hostApp = express();
@@ -34,6 +35,11 @@ hostApp.use((req, res, next) => {
3435

3536
hostApp.use(express.static(DIRECTORY));
3637

38+
// API endpoint to get configured server URLs
39+
hostApp.get("/api/servers", (_req, res) => {
40+
res.json(SERVERS);
41+
});
42+
3743
hostApp.get("/", (_req, res) => {
3844
res.redirect("/index.html");
3945
});
@@ -70,15 +76,15 @@ sandboxApp.use((_req, res) => {
7076
});
7177

7278
// ============ Start both servers ============
73-
hostApp.listen(HOST_PORT, err => {
79+
hostApp.listen(HOST_PORT, (err) => {
7480
if (err) {
7581
console.error("Error starting server:", err);
7682
process.exit(1);
7783
}
7884
console.log(`Host server: http://localhost:${HOST_PORT}`);
7985
});
8086

81-
sandboxApp.listen(SANDBOX_PORT, err => {
87+
sandboxApp.listen(SANDBOX_PORT, (err) => {
8288
if (err) {
8389
console.error("Error starting server:", err);
8490
process.exit(1);

examples/basic-host/src/implementation.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const log = {
1717

1818

1919
export interface ServerInfo {
20+
name: string;
2021
client: Client;
2122
tools: Map<string, Tool>;
2223
appHtmlCache: Map<string, string>;
@@ -30,11 +31,13 @@ export async function connectToServer(serverUrl: URL): Promise<ServerInfo> {
3031
await client.connect(new StreamableHTTPClientTransport(serverUrl));
3132
log.info("Connection successful");
3233

34+
const name = client.getServerVersion()?.name ?? serverUrl.href;
35+
3336
const toolsList = await client.listTools();
3437
const tools = new Map(toolsList.tools.map((tool) => [tool.name, tool]));
3538
log.info("Server tools:", Array.from(tools.keys()));
3639

37-
return { client, tools, appHtmlCache: new Map() };
40+
return { name, client, tools, appHtmlCache: new Map() };
3841
}
3942

4043

examples/basic-host/src/index.tsx

Lines changed: 41 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,114 +4,80 @@ import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy,
44
import styles from "./index.module.css";
55

66

7-
// Available MCP servers - using ports 3101+ to avoid conflicts with common dev ports
8-
const SERVERS = [
9-
{ name: "Basic React", port: 3101 },
10-
{ name: "Vanilla JS", port: 3102 },
11-
{ name: "Budget Allocator", port: 3103 },
12-
{ name: "Cohort Heatmap", port: 3104 },
13-
{ name: "Customer Segmentation", port: 3105 },
14-
{ name: "Scenario Modeler", port: 3106 },
15-
{ name: "System Monitor", port: 3107 },
16-
{ name: "Three.js", port: 3109 },
17-
] as const;
18-
19-
function serverUrl(port: number): string {
20-
return `http://localhost:${port}/mcp`;
21-
}
22-
23-
// Cache server connections to avoid reconnecting when switching between servers
24-
const serverInfoCache = new Map<number, Promise<ServerInfo>>();
25-
26-
function getServerInfo(port: number): Promise<ServerInfo> {
27-
let promise = serverInfoCache.get(port);
28-
if (!promise) {
29-
promise = connectToServer(new URL(serverUrl(port)));
30-
// Remove from cache on failure so retry is possible
31-
promise.catch(() => serverInfoCache.delete(port));
32-
serverInfoCache.set(port, promise);
33-
}
34-
return promise;
35-
}
36-
37-
387
// Wrapper to track server name with each tool call
398
interface ToolCallEntry {
409
serverName: string;
4110
info: ToolCallInfo;
4211
}
4312

44-
// Host just manages tool call results - no server dependency
45-
function Host() {
13+
// Host receives connected servers via promise, uses single use() call
14+
interface HostProps {
15+
serversPromise: Promise<ServerInfo[]>;
16+
}
17+
function Host({ serversPromise }: HostProps) {
18+
const servers = use(serversPromise);
4619
const [toolCalls, setToolCalls] = useState<ToolCallEntry[]>([]);
4720

21+
if (servers.length === 0) {
22+
return <p>No servers configured. Set SERVERS environment variable.</p>;
23+
}
24+
4825
return (
4926
<>
5027
{toolCalls.map((entry, i) => (
5128
<ToolCallInfoPanel key={i} serverName={entry.serverName} toolCallInfo={entry.info} />
5229
))}
5330
<CallToolPanel
31+
servers={servers}
5432
addToolCall={(serverName, info) => setToolCalls([...toolCalls, { serverName, info }])}
5533
/>
5634
</>
5735
);
5836
}
5937

6038

61-
// CallToolPanel includes server selection with its own Suspense boundary
39+
// CallToolPanel manages server selection from already-connected servers
6240
interface CallToolPanelProps {
41+
servers: ServerInfo[];
6342
addToolCall: (serverName: string, info: ToolCallInfo) => void;
6443
}
65-
function CallToolPanel({ addToolCall }: CallToolPanelProps) {
66-
const [selectedServer, setSelectedServer] = useState(SERVERS[0]);
67-
const [serverInfoPromise, setServerInfoPromise] = useState(
68-
() => getServerInfo(selectedServer.port)
69-
);
70-
71-
const handleServerChange = (port: number) => {
72-
const server = SERVERS.find(s => s.port === port) ?? SERVERS[0];
73-
setSelectedServer(server);
74-
setServerInfoPromise(getServerInfo(port));
75-
};
44+
function CallToolPanel({ servers, addToolCall }: CallToolPanelProps) {
45+
const [selectedIndex, setSelectedIndex] = useState(0);
46+
const selectedServer = servers[selectedIndex];
7647

7748
return (
7849
<div className={styles.callToolPanel}>
7950
<label>
8051
Server
8152
<select
82-
value={selectedServer.port}
83-
onChange={(e) => handleServerChange(Number(e.target.value))}
53+
value={selectedIndex}
54+
onChange={(e) => setSelectedIndex(Number(e.target.value))}
8455
>
85-
{SERVERS.map(({ name, port }) => (
86-
<option key={port} value={port}>
87-
{name} (:{port})
56+
{servers.map((server, i) => (
57+
<option key={i} value={i}>
58+
{server.name}
8859
</option>
8960
))}
9061
</select>
9162
</label>
92-
<ErrorBoundary>
93-
<Suspense fallback={<p className={styles.connecting}>Connecting to {serverUrl(selectedServer.port)}...</p>}>
94-
<ToolCallForm
95-
key={selectedServer.port}
96-
serverName={selectedServer.name}
97-
serverInfoPromise={serverInfoPromise}
98-
addToolCall={addToolCall}
99-
/>
100-
</Suspense>
101-
</ErrorBoundary>
63+
<ToolCallForm
64+
key={selectedIndex}
65+
serverName={selectedServer.name}
66+
serverInfo={selectedServer}
67+
addToolCall={addToolCall}
68+
/>
10269
</div>
10370
);
10471
}
10572

10673

107-
// ToolCallForm renders inside Suspense - needs serverInfo for tool list
74+
// ToolCallForm receives already-resolved serverInfo
10875
interface ToolCallFormProps {
10976
serverName: string;
110-
serverInfoPromise: Promise<ServerInfo>;
77+
serverInfo: ServerInfo;
11178
addToolCall: (serverName: string, info: ToolCallInfo) => void;
11279
}
113-
function ToolCallForm({ serverName, serverInfoPromise, addToolCall }: ToolCallFormProps) {
114-
const serverInfo = use(serverInfoPromise);
80+
function ToolCallForm({ serverName, serverInfo, addToolCall }: ToolCallFormProps) {
11581
const toolNames = Array.from(serverInfo.tools.keys());
11682
const [selectedTool, setSelectedTool] = useState(toolNames[0] ?? "");
11783
const [inputJson, setInputJson] = useState("{}");
@@ -264,8 +230,18 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
264230
}
265231

266232

233+
async function connectToAllServers(): Promise<ServerInfo[]> {
234+
const serverUrlsResponse = await fetch("/api/servers");
235+
const serverUrls = (await serverUrlsResponse.json()) as string[];
236+
return Promise.all(serverUrls.map((url) => connectToServer(new URL(url))));
237+
}
238+
267239
createRoot(document.getElementById("root")!).render(
268240
<StrictMode>
269-
<Host />
241+
<ErrorBoundary>
242+
<Suspense fallback={<p className={styles.connecting}>Connecting to servers...</p>}>
243+
<Host serversPromise={connectToAllServers()} />
244+
</Suspense>
245+
</ErrorBoundary>
270246
</StrictMode>,
271247
);

examples/basic-server-react/server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const DIST_DIR = path.join(import.meta.dirname, "dist");
1212

1313

1414
const server = new McpServer({
15-
name: "MCP App Server",
15+
name: "Basic MCP App Server (React-based)",
1616
version: "1.0.0",
1717
});
1818

@@ -85,7 +85,7 @@ app.post("/mcp", async (req: Request, res: Response) => {
8585
}
8686
});
8787

88-
const httpServer = app.listen(PORT, err => {
88+
const httpServer = app.listen(PORT, (err) => {
8989
if (err) {
9090
console.error("Error starting server:", err);
9191
process.exit(1);

examples/basic-server-vanillajs/server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const DIST_DIR = path.join(import.meta.dirname, "dist");
1212

1313

1414
const server = new McpServer({
15-
name: "MCP App Server",
15+
name: "Basic MCP App Server (Vanilla JS)",
1616
version: "1.0.0",
1717
});
1818

@@ -85,7 +85,7 @@ app.post("/mcp", async (req: Request, res: Response) => {
8585
}
8686
});
8787

88-
const httpServer = app.listen(PORT, err => {
88+
const httpServer = app.listen(PORT, (err) => {
8989
if (err) {
9090
console.error("Error starting server:", err);
9191
process.exit(1);

examples/run-all.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env bun
2+
/**
3+
* Orchestration script for running all example servers.
4+
*
5+
* Usage:
6+
* bun examples/run-all.ts start - Build and start all examples
7+
* bun examples/run-all.ts dev - Run all examples in dev/watch mode
8+
* bun examples/run-all.ts build - Build all examples
9+
*/
10+
11+
import { readdirSync, statSync, existsSync } from "fs";
12+
import { spawn, type ChildProcess } from "child_process";
13+
14+
const BASE_PORT = 3101;
15+
const BASIC_HOST = "basic-host";
16+
17+
// Find all example directories except basic-host that have a package.json,
18+
// assign ports, and build URL list
19+
const servers = readdirSync("examples")
20+
.filter(
21+
(d) =>
22+
d !== BASIC_HOST &&
23+
statSync(`examples/${d}`).isDirectory() &&
24+
existsSync(`examples/${d}/package.json`),
25+
)
26+
.sort() // Sort for consistent port assignment
27+
.map((dir, i) => ({
28+
dir,
29+
port: BASE_PORT + i,
30+
url: `http://localhost:${BASE_PORT + i}/mcp`,
31+
}));
32+
33+
const COMMANDS = ["start", "dev", "build"];
34+
35+
const command = process.argv[2];
36+
37+
if (!command || !COMMANDS.includes(command)) {
38+
console.error(`Usage: bun examples/run-all.ts <${COMMANDS.join("|")}>`);
39+
40+
process.exit(1);
41+
}
42+
43+
const processes: ChildProcess[] = [];
44+
45+
// Handle cleanup on exit
46+
function cleanup() {
47+
for (const proc of processes) {
48+
proc.kill();
49+
}
50+
}
51+
process.on("SIGINT", cleanup);
52+
process.on("SIGTERM", cleanup);
53+
54+
// Spawn a process and track it
55+
function spawnProcess(
56+
cmd: string,
57+
args: string[],
58+
env: Record<string, string> = {},
59+
prefix: string,
60+
): ChildProcess {
61+
const proc = spawn(cmd, args, {
62+
env: { ...process.env, ...env },
63+
stdio: ["ignore", "pipe", "pipe"],
64+
});
65+
66+
proc.stdout?.on("data", (data) => {
67+
const lines = data.toString().trim().split("\n");
68+
for (const line of lines) {
69+
console.log(`[${prefix}] ${line}`);
70+
}
71+
});
72+
73+
proc.stderr?.on("data", (data) => {
74+
const lines = data.toString().trim().split("\n");
75+
for (const line of lines) {
76+
console.error(`[${prefix}] ${line}`);
77+
}
78+
});
79+
80+
proc.on("exit", (code) => {
81+
if (code !== 0 && code !== null) {
82+
console.error(`[${prefix}] exited with code ${code}`);
83+
}
84+
});
85+
86+
processes.push(proc);
87+
return proc;
88+
}
89+
90+
// Build the SERVERS environment variable (JSON array of URLs)
91+
const serversEnv = JSON.stringify(servers.map((s) => s.url));
92+
93+
console.log(`Running command: ${command}`);
94+
console.log(
95+
`Server examples: ${servers.map((s) => `${s.dir}:${s.port}`).join(", ")}`,
96+
);
97+
console.log("");
98+
99+
// If dev mode, also run the main library watcher
100+
if (command === "dev") {
101+
spawnProcess("npm", ["run", "watch"], {}, "lib");
102+
}
103+
104+
// Run each server example
105+
for (const { dir, port } of servers) {
106+
spawnProcess(
107+
"npm",
108+
["run", "--workspace", `examples/${dir}`, command],
109+
{ PORT: String(port) },
110+
dir,
111+
);
112+
}
113+
114+
// Run basic-host with the SERVERS env var
115+
spawnProcess(
116+
"npm",
117+
["run", "--workspace", `examples/${BASIC_HOST}`, command],
118+
{ SERVERS: serversEnv },
119+
BASIC_HOST,
120+
);

0 commit comments

Comments
 (0)