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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3dde360dc9
commit
f6ebefe238
3 changed files with 137 additions and 48 deletions
|
|
@ -1,3 +1,9 @@
|
||||||
|
.connecting {
|
||||||
|
padding: 1rem 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
.callToolPanel, .toolCallInfoPanel {
|
.callToolPanel, .toolCallInfoPanel {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|
@ -10,6 +16,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.callToolPanel {
|
.callToolPanel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
max-width: 480px;
|
max-width: 480px;
|
||||||
|
|
||||||
form {
|
form {
|
||||||
|
|
|
||||||
|
|
@ -4,35 +4,113 @@ import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy,
|
||||||
import styles from "./index.module.css";
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
|
|
||||||
const MCP_SERVER_URL = new URL("http://localhost:3001/mcp");
|
// 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 },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function serverUrl(port: number): string {
|
||||||
interface HostProps {
|
return `http://localhost:${port}/mcp`;
|
||||||
serverInfoPromise: Promise<ServerInfo>;
|
|
||||||
}
|
}
|
||||||
function Host({ serverInfoPromise }: HostProps) {
|
|
||||||
const serverInfo = use(serverInfoPromise);
|
// Cache server connections to avoid reconnecting when switching between servers
|
||||||
const [toolCallInfos, setToolCallInfos] = useState<ToolCallInfo[]>([]);
|
const serverInfoCache = new Map<number, Promise<ServerInfo>>();
|
||||||
|
|
||||||
|
function getServerInfo(port: number): Promise<ServerInfo> {
|
||||||
|
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() {
|
||||||
|
const [toolCalls, setToolCalls] = useState<ToolCallEntry[]>([]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{toolCallInfos.map((info, i) => (
|
{toolCalls.map((entry, i) => (
|
||||||
<ToolCallInfoPanel key={i} toolCallInfo={info} />
|
<ToolCallInfoPanel key={i} serverName={entry.serverName} toolCallInfo={entry.info} />
|
||||||
))}
|
))}
|
||||||
<CallToolPanel
|
<CallToolPanel
|
||||||
serverInfo={serverInfo}
|
addToolCall={(serverName, info) => setToolCalls([...toolCalls, { serverName, info }])}
|
||||||
addToolCallInfo={(info) => setToolCallInfos([...toolCallInfos, info])}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// CallToolPanel includes server selection with its own Suspense boundary
|
||||||
interface CallToolPanelProps {
|
interface CallToolPanelProps {
|
||||||
serverInfo: ServerInfo;
|
addToolCall: (serverName: string, info: ToolCallInfo) => void;
|
||||||
addToolCallInfo: (toolCallInfo: ToolCallInfo) => void;
|
|
||||||
}
|
}
|
||||||
function CallToolPanel({ serverInfo, addToolCallInfo }: CallToolPanelProps) {
|
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));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.callToolPanel}>
|
||||||
|
<label>
|
||||||
|
Server
|
||||||
|
<select
|
||||||
|
value={selectedServer.port}
|
||||||
|
onChange={(e) => handleServerChange(Number(e.target.value))}
|
||||||
|
>
|
||||||
|
{SERVERS.map(({ name, port }) => (
|
||||||
|
<option key={port} value={port}>
|
||||||
|
{name} (:{port})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Suspense fallback={<p className={styles.connecting}>Connecting to {serverUrl(selectedServer.port)}...</p>}>
|
||||||
|
<ToolCallForm
|
||||||
|
key={selectedServer.port}
|
||||||
|
serverName={selectedServer.name}
|
||||||
|
serverInfoPromise={serverInfoPromise}
|
||||||
|
addToolCall={addToolCall}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ToolCallForm renders inside Suspense - needs serverInfo for tool list
|
||||||
|
interface ToolCallFormProps {
|
||||||
|
serverName: string;
|
||||||
|
serverInfoPromise: Promise<ServerInfo>;
|
||||||
|
addToolCall: (serverName: string, info: ToolCallInfo) => void;
|
||||||
|
}
|
||||||
|
function ToolCallForm({ serverName, serverInfoPromise, addToolCall }: ToolCallFormProps) {
|
||||||
|
const serverInfo = use(serverInfoPromise);
|
||||||
const toolNames = Array.from(serverInfo.tools.keys());
|
const toolNames = Array.from(serverInfo.tools.keys());
|
||||||
const [selectedTool, setSelectedTool] = useState(toolNames[0] ?? "");
|
const [selectedTool, setSelectedTool] = useState(toolNames[0] ?? "");
|
||||||
const [inputJson, setInputJson] = useState("{}");
|
const [inputJson, setInputJson] = useState("{}");
|
||||||
|
|
@ -48,14 +126,13 @@ function CallToolPanel({ serverInfo, addToolCallInfo }: CallToolPanelProps) {
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
const toolCallInfo = callTool(serverInfo, selectedTool, JSON.parse(inputJson));
|
const toolCallInfo = callTool(serverInfo, selectedTool, JSON.parse(inputJson));
|
||||||
addToolCallInfo(toolCallInfo);
|
addToolCall(serverName, toolCallInfo);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.callToolPanel}>
|
|
||||||
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
||||||
<label>
|
<label>
|
||||||
Tool Name
|
Tool
|
||||||
<select
|
<select
|
||||||
value={selectedTool}
|
value={selectedTool}
|
||||||
onChange={(e) => setSelectedTool(e.target.value)}
|
onChange={(e) => setSelectedTool(e.target.value)}
|
||||||
|
|
@ -66,7 +143,7 @@ function CallToolPanel({ serverInfo, addToolCallInfo }: CallToolPanelProps) {
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Tool Input
|
Input
|
||||||
<textarea
|
<textarea
|
||||||
aria-invalid={!isValidJson}
|
aria-invalid={!isValidJson}
|
||||||
value={inputJson}
|
value={inputJson}
|
||||||
|
|
@ -77,19 +154,19 @@ function CallToolPanel({ serverInfo, addToolCallInfo }: CallToolPanelProps) {
|
||||||
Call Tool
|
Call Tool
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface ToolCallInfoPanelProps {
|
interface ToolCallInfoPanelProps {
|
||||||
|
serverName: string;
|
||||||
toolCallInfo: ToolCallInfo;
|
toolCallInfo: ToolCallInfo;
|
||||||
}
|
}
|
||||||
function ToolCallInfoPanel({ toolCallInfo }: ToolCallInfoPanelProps) {
|
function ToolCallInfoPanel({ serverName, toolCallInfo }: ToolCallInfoPanelProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.toolCallInfoPanel}>
|
<div className={styles.toolCallInfoPanel}>
|
||||||
<div className={styles.inputInfoPanel}>
|
<div className={styles.inputInfoPanel}>
|
||||||
<h2 className={styles.toolName}>{toolCallInfo.tool.name}</h2>
|
<h2 className={styles.toolName}>{serverName}:{toolCallInfo.tool.name}</h2>
|
||||||
<JsonBlock value={toolCallInfo.input} />
|
<JsonBlock value={toolCallInfo.input} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.outputInfoPanel}>
|
<div className={styles.outputInfoPanel}>
|
||||||
|
|
@ -188,8 +265,6 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<Suspense fallback={<p>Connecting to server ({MCP_SERVER_URL.href})...</p>}>
|
<Host />
|
||||||
<Host serverInfoPromise={connectToServer(MCP_SERVER_URL)} />
|
|
||||||
</Suspense>
|
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
11
package.json
11
package.json
|
|
@ -30,10 +30,15 @@
|
||||||
"build:all": "npm run build && npm run examples:build",
|
"build:all": "npm run build && npm run examples:build",
|
||||||
"test": "bun test",
|
"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: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'",
|
"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'",
|
||||||
"examples:start:basic-host": "npm run --workspace=examples/basic-host start",
|
"examples:start:basic-host": "npm run --workspace=examples/basic-host start",
|
||||||
"examples:start:basic-server-react": "npm run --workspace=examples/basic-server-react start",
|
"examples:start:basic-server-react": "PORT=3101 npm run --workspace=examples/basic-server-react start",
|
||||||
"examples:start:basic-server-vanillajs": "npm run --workspace=examples/basic-server-vanillajs 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",
|
||||||
"watch": "nodemon --watch src --ext ts,tsx --exec 'bun build.bun.ts'",
|
"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": "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-host": "npm run --workspace=examples/basic-host dev",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue