Skip to content

Commit a01dd0e

Browse files
ochafikclaude
andcommitted
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]>
1 parent 0a5a990 commit a01dd0e

File tree

2 files changed

+110
-116
lines changed

2 files changed

+110
-116
lines changed

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

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,5 @@
1-
.serverSelector {
2-
max-width: 480px;
3-
margin: 0 auto 1rem;
4-
padding: 1rem;
5-
border: 1px solid #ddd;
6-
border-radius: 4px;
7-
background-color: #f9f9f9;
8-
9-
label {
10-
display: flex;
11-
flex-direction: column;
12-
gap: 0.25rem;
13-
font-weight: 600;
14-
}
15-
16-
select {
17-
padding: 0.5rem;
18-
border: 1px solid #ccc;
19-
border-radius: 4px;
20-
font-family: monospace;
21-
font-size: inherit;
22-
}
23-
}
24-
251
.connecting {
26-
max-width: 480px;
27-
margin: 0 auto;
28-
padding: 1rem;
2+
padding: 1rem 0;
293
text-align: center;
304
color: #666;
315
}
@@ -42,6 +16,9 @@
4216
}
4317

4418
.callToolPanel {
19+
display: flex;
20+
flex-direction: column;
21+
gap: 1rem;
4522
max-width: 480px;
4623

4724
form {

examples/basic-host/src/index.tsx

Lines changed: 106 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -19,33 +19,98 @@ function serverUrl(port: number): string {
1919
return `http://localhost:${port}/mcp`;
2020
}
2121

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;
34+
}
2235

23-
interface HostProps {
24-
serverInfoPromise: Promise<ServerInfo>;
36+
37+
// Wrapper to track server name with each tool call
38+
interface ToolCallEntry {
39+
serverName: string;
40+
info: ToolCallInfo;
2541
}
26-
function Host({ serverInfoPromise }: HostProps) {
27-
const serverInfo = use(serverInfoPromise);
28-
const [toolCallInfos, setToolCallInfos] = useState<ToolCallInfo[]>([]);
42+
43+
// Host just manages tool call results - no server dependency
44+
function Host() {
45+
const [toolCalls, setToolCalls] = useState<ToolCallEntry[]>([]);
2946

3047
return (
3148
<>
32-
{toolCallInfos.map((info, i) => (
33-
<ToolCallInfoPanel key={i} toolCallInfo={info} />
49+
{toolCalls.map((entry, i) => (
50+
<ToolCallInfoPanel key={i} serverName={entry.serverName} toolCallInfo={entry.info} />
3451
))}
3552
<CallToolPanel
36-
serverInfo={serverInfo}
37-
addToolCallInfo={(info) => setToolCallInfos([...toolCallInfos, info])}
53+
addToolCall={(serverName, info) => setToolCalls([...toolCalls, { serverName, info }])}
3854
/>
3955
</>
4056
);
4157
}
4258

4359

60+
// CallToolPanel includes server selection with its own Suspense boundary
4461
interface CallToolPanelProps {
45-
serverInfo: ServerInfo;
46-
addToolCallInfo: (toolCallInfo: ToolCallInfo) => void;
62+
addToolCall: (serverName: string, info: ToolCallInfo) => void;
63+
}
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+
);
47103
}
48-
function CallToolPanel({ serverInfo, addToolCallInfo }: CallToolPanelProps) {
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);
49114
const toolNames = Array.from(serverInfo.tools.keys());
50115
const [selectedTool, setSelectedTool] = useState(toolNames[0] ?? "");
51116
const [inputJson, setInputJson] = useState("{}");
@@ -61,48 +126,47 @@ function CallToolPanel({ serverInfo, addToolCallInfo }: CallToolPanelProps) {
61126

62127
const handleSubmit = () => {
63128
const toolCallInfo = callTool(serverInfo, selectedTool, JSON.parse(inputJson));
64-
addToolCallInfo(toolCallInfo);
129+
addToolCall(serverName, toolCallInfo);
65130
};
66131

67132
return (
68-
<div className={styles.callToolPanel}>
69-
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
70-
<label>
71-
Tool Name
72-
<select
73-
value={selectedTool}
74-
onChange={(e) => setSelectedTool(e.target.value)}
75-
>
76-
{toolNames.map((name) => (
77-
<option key={name} value={name}>{name}</option>
78-
))}
79-
</select>
80-
</label>
81-
<label>
82-
Tool Input
83-
<textarea
84-
aria-invalid={!isValidJson}
85-
value={inputJson}
86-
onChange={(e) => setInputJson(e.target.value)}
87-
/>
88-
</label>
89-
<button type="submit" disabled={!selectedTool || !isValidJson}>
90-
Call Tool
91-
</button>
92-
</form>
93-
</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>
94157
);
95158
}
96159

97160

98161
interface ToolCallInfoPanelProps {
162+
serverName: string;
99163
toolCallInfo: ToolCallInfo;
100164
}
101-
function ToolCallInfoPanel({ toolCallInfo }: ToolCallInfoPanelProps) {
165+
function ToolCallInfoPanel({ serverName, toolCallInfo }: ToolCallInfoPanelProps) {
102166
return (
103167
<div className={styles.toolCallInfoPanel}>
104168
<div className={styles.inputInfoPanel}>
105-
<h2 className={styles.toolName}>{toolCallInfo.tool.name}</h2>
169+
<h2 className={styles.toolName}>{serverName}:{toolCallInfo.tool.name}</h2>
106170
<JsonBlock value={toolCallInfo.input} />
107171
</div>
108172
<div className={styles.outputInfoPanel}>
@@ -199,55 +263,8 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
199263
}
200264

201265

202-
interface ServerSelectorProps {
203-
selectedPort: number;
204-
onSelect: (port: number) => void;
205-
}
206-
function ServerSelector({ selectedPort, onSelect }: ServerSelectorProps) {
207-
return (
208-
<div className={styles.serverSelector}>
209-
<label>
210-
Server
211-
<select
212-
value={selectedPort}
213-
onChange={(e) => onSelect(Number(e.target.value))}
214-
>
215-
{SERVERS.map(({ name, port }) => (
216-
<option key={port} value={port}>
217-
{name} (:{port})
218-
</option>
219-
))}
220-
</select>
221-
</label>
222-
</div>
223-
);
224-
}
225-
226-
227-
function App() {
228-
const [selectedPort, setSelectedPort] = useState(SERVERS[0].port);
229-
const [serverInfoPromise, setServerInfoPromise] = useState(
230-
() => connectToServer(new URL(serverUrl(selectedPort)))
231-
);
232-
233-
const handleServerChange = (port: number) => {
234-
setSelectedPort(port);
235-
setServerInfoPromise(connectToServer(new URL(serverUrl(port))));
236-
};
237-
238-
return (
239-
<>
240-
<ServerSelector selectedPort={selectedPort} onSelect={handleServerChange} />
241-
<Suspense fallback={<p className={styles.connecting}>Connecting to {serverUrl(selectedPort)}...</p>}>
242-
<Host key={selectedPort} serverInfoPromise={serverInfoPromise} />
243-
</Suspense>
244-
</>
245-
);
246-
}
247-
248-
249266
createRoot(document.getElementById("root")!).render(
250267
<StrictMode>
251-
<App />
268+
<Host />
252269
</StrictMode>,
253270
);

0 commit comments

Comments
 (0)