Skip to content

Commit f6ebefe

Browse files
ochafikclaude
andauthored
Add server selector to basic-host example (#95)
* Add server selector to basic-host example - Add ServerSelector component to switch between MCP servers - Run all example servers concurrently on ports 3101-3107 - Update examples:start to launch all servers with assigned ports 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Move server selector into tool call panel - Server selector now in same component as tool selector - Tool call results persist across server switches - Connection caching avoids reconnects when switching servers - Display server name with tool name in results (ServerName:ToolName) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 3dde360 commit f6ebefe

File tree

3 files changed

+137
-48
lines changed

3 files changed

+137
-48
lines changed

examples/basic-host/src/index.module.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
.connecting {
2+
padding: 1rem 0;
3+
text-align: center;
4+
color: #666;
5+
}
6+
17
.callToolPanel, .toolCallInfoPanel {
28
margin: 0 auto;
39
padding: 1rem;
@@ -10,6 +16,9 @@
1016
}
1117

1218
.callToolPanel {
19+
display: flex;
20+
flex-direction: column;
21+
gap: 1rem;
1322
max-width: 480px;
1423

1524
form {

examples/basic-host/src/index.tsx

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

66

7-
const MCP_SERVER_URL = new URL("http://localhost:3001/mcp");
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+
] as const;
817

18+
function serverUrl(port: number): string {
19+
return `http://localhost:${port}/mcp`;
20+
}
921

10-
interface HostProps {
11-
serverInfoPromise: Promise<ServerInfo>;
22+
// Cache server connections to avoid reconnecting when switching between servers
23+
const serverInfoCache = new Map<number, Promise<ServerInfo>>();
24+
25+
function getServerInfo(port: number): Promise<ServerInfo> {
26+
let promise = serverInfoCache.get(port);
27+
if (!promise) {
28+
promise = connectToServer(new URL(serverUrl(port)));
29+
// Remove from cache on failure so retry is possible
30+
promise.catch(() => serverInfoCache.delete(port));
31+
serverInfoCache.set(port, promise);
32+
}
33+
return promise;
1234
}
13-
function Host({ serverInfoPromise }: HostProps) {
14-
const serverInfo = use(serverInfoPromise);
15-
const [toolCallInfos, setToolCallInfos] = useState<ToolCallInfo[]>([]);
35+
36+
37+
// Wrapper to track server name with each tool call
38+
interface ToolCallEntry {
39+
serverName: string;
40+
info: ToolCallInfo;
41+
}
42+
43+
// Host just manages tool call results - no server dependency
44+
function Host() {
45+
const [toolCalls, setToolCalls] = useState<ToolCallEntry[]>([]);
1646

1747
return (
1848
<>
19-
{toolCallInfos.map((info, i) => (
20-
<ToolCallInfoPanel key={i} toolCallInfo={info} />
49+
{toolCalls.map((entry, i) => (
50+
<ToolCallInfoPanel key={i} serverName={entry.serverName} toolCallInfo={entry.info} />
2151
))}
2252
<CallToolPanel
23-
serverInfo={serverInfo}
24-
addToolCallInfo={(info) => setToolCallInfos([...toolCallInfos, info])}
53+
addToolCall={(serverName, info) => setToolCalls([...toolCalls, { serverName, info }])}
2554
/>
2655
</>
2756
);
2857
}
2958

3059

60+
// CallToolPanel includes server selection with its own Suspense boundary
3161
interface CallToolPanelProps {
32-
serverInfo: ServerInfo;
33-
addToolCallInfo: (toolCallInfo: ToolCallInfo) => void;
62+
addToolCall: (serverName: string, info: ToolCallInfo) => void;
3463
}
35-
function CallToolPanel({ serverInfo, addToolCallInfo }: CallToolPanelProps) {
64+
function CallToolPanel({ addToolCall }: CallToolPanelProps) {
65+
const [selectedServer, setSelectedServer] = useState(SERVERS[0]);
66+
const [serverInfoPromise, setServerInfoPromise] = useState(
67+
() => getServerInfo(selectedServer.port)
68+
);
69+
70+
const handleServerChange = (port: number) => {
71+
const server = SERVERS.find(s => s.port === port) ?? SERVERS[0];
72+
setSelectedServer(server);
73+
setServerInfoPromise(getServerInfo(port));
74+
};
75+
76+
return (
77+
<div className={styles.callToolPanel}>
78+
<label>
79+
Server
80+
<select
81+
value={selectedServer.port}
82+
onChange={(e) => handleServerChange(Number(e.target.value))}
83+
>
84+
{SERVERS.map(({ name, port }) => (
85+
<option key={port} value={port}>
86+
{name} (:{port})
87+
</option>
88+
))}
89+
</select>
90+
</label>
91+
<ErrorBoundary>
92+
<Suspense fallback={<p className={styles.connecting}>Connecting to {serverUrl(selectedServer.port)}...</p>}>
93+
<ToolCallForm
94+
key={selectedServer.port}
95+
serverName={selectedServer.name}
96+
serverInfoPromise={serverInfoPromise}
97+
addToolCall={addToolCall}
98+
/>
99+
</Suspense>
100+
</ErrorBoundary>
101+
</div>
102+
);
103+
}
104+
105+
106+
// ToolCallForm renders inside Suspense - needs serverInfo for tool list
107+
interface ToolCallFormProps {
108+
serverName: string;
109+
serverInfoPromise: Promise<ServerInfo>;
110+
addToolCall: (serverName: string, info: ToolCallInfo) => void;
111+
}
112+
function ToolCallForm({ serverName, serverInfoPromise, addToolCall }: ToolCallFormProps) {
113+
const serverInfo = use(serverInfoPromise);
36114
const toolNames = Array.from(serverInfo.tools.keys());
37115
const [selectedTool, setSelectedTool] = useState(toolNames[0] ?? "");
38116
const [inputJson, setInputJson] = useState("{}");
@@ -48,48 +126,47 @@ function CallToolPanel({ serverInfo, addToolCallInfo }: CallToolPanelProps) {
48126

49127
const handleSubmit = () => {
50128
const toolCallInfo = callTool(serverInfo, selectedTool, JSON.parse(inputJson));
51-
addToolCallInfo(toolCallInfo);
129+
addToolCall(serverName, toolCallInfo);
52130
};
53131

54132
return (
55-
<div className={styles.callToolPanel}>
56-
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
57-
<label>
58-
Tool Name
59-
<select
60-
value={selectedTool}
61-
onChange={(e) => setSelectedTool(e.target.value)}
62-
>
63-
{toolNames.map((name) => (
64-
<option key={name} value={name}>{name}</option>
65-
))}
66-
</select>
67-
</label>
68-
<label>
69-
Tool Input
70-
<textarea
71-
aria-invalid={!isValidJson}
72-
value={inputJson}
73-
onChange={(e) => setInputJson(e.target.value)}
74-
/>
75-
</label>
76-
<button type="submit" disabled={!selectedTool || !isValidJson}>
77-
Call Tool
78-
</button>
79-
</form>
80-
</div>
133+
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
134+
<label>
135+
Tool
136+
<select
137+
value={selectedTool}
138+
onChange={(e) => setSelectedTool(e.target.value)}
139+
>
140+
{toolNames.map((name) => (
141+
<option key={name} value={name}>{name}</option>
142+
))}
143+
</select>
144+
</label>
145+
<label>
146+
Input
147+
<textarea
148+
aria-invalid={!isValidJson}
149+
value={inputJson}
150+
onChange={(e) => setInputJson(e.target.value)}
151+
/>
152+
</label>
153+
<button type="submit" disabled={!selectedTool || !isValidJson}>
154+
Call Tool
155+
</button>
156+
</form>
81157
);
82158
}
83159

84160

85161
interface ToolCallInfoPanelProps {
162+
serverName: string;
86163
toolCallInfo: ToolCallInfo;
87164
}
88-
function ToolCallInfoPanel({ toolCallInfo }: ToolCallInfoPanelProps) {
165+
function ToolCallInfoPanel({ serverName, toolCallInfo }: ToolCallInfoPanelProps) {
89166
return (
90167
<div className={styles.toolCallInfoPanel}>
91168
<div className={styles.inputInfoPanel}>
92-
<h2 className={styles.toolName}>{toolCallInfo.tool.name}</h2>
169+
<h2 className={styles.toolName}>{serverName}:{toolCallInfo.tool.name}</h2>
93170
<JsonBlock value={toolCallInfo.input} />
94171
</div>
95172
<div className={styles.outputInfoPanel}>
@@ -188,8 +265,6 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
188265

189266
createRoot(document.getElementById("root")!).render(
190267
<StrictMode>
191-
<Suspense fallback={<p>Connecting to server ({MCP_SERVER_URL.href})...</p>}>
192-
<Host serverInfoPromise={connectToServer(MCP_SERVER_URL)} />
193-
</Suspense>
268+
<Host />
194269
</StrictMode>,
195270
);

package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,15 @@
3030
"build:all": "npm run build && npm run examples:build",
3131
"test": "bun test",
3232
"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",
33-
"examples:start": "NODE_ENV=development npm run build && concurrently 'npm run examples:start:basic-host' 'npm run examples:start:basic-server-react'",
33+
"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'",
3434
"examples:start:basic-host": "npm run --workspace=examples/basic-host start",
35-
"examples:start:basic-server-react": "npm run --workspace=examples/basic-server-react start",
36-
"examples:start:basic-server-vanillajs": "npm run --workspace=examples/basic-server-vanillajs start",
35+
"examples:start:basic-server-react": "PORT=3101 npm run --workspace=examples/basic-server-react start",
36+
"examples:start:basic-server-vanillajs": "PORT=3102 npm run --workspace=examples/basic-server-vanillajs start",
37+
"examples:start:budget-allocator-server": "PORT=3103 npm run --workspace=examples/budget-allocator-server start",
38+
"examples:start:cohort-heatmap-server": "PORT=3104 npm run --workspace=examples/cohort-heatmap-server start",
39+
"examples:start:customer-segmentation-server": "PORT=3105 npm run --workspace=examples/customer-segmentation-server start",
40+
"examples:start:scenario-modeler-server": "PORT=3106 npm run --workspace=examples/scenario-modeler-server start",
41+
"examples:start:system-monitor-server": "PORT=3107 npm run --workspace=examples/system-monitor-server start",
3742
"watch": "nodemon --watch src --ext ts,tsx --exec 'bun build.bun.ts'",
3843
"examples:dev": "NODE_ENV=development concurrently 'npm run watch' 'npm run examples:dev:basic-host' 'npm run examples:dev:basic-server-react'",
3944
"examples:dev:basic-host": "npm run --workspace=examples/basic-host dev",

0 commit comments

Comments
 (0)