diff --git a/examples/basic-host/serve.ts b/examples/basic-host/serve.ts index 450ec571..1b9f3cc2 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 614aeead..3fc84637 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.module.css b/examples/basic-host/src/index.module.css index 961d078f..c162aa8c 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 5a346fc7..37eac43f 100644 --- a/examples/basic-host/src/index.tsx +++ b/examples/basic-host/src/index.tsx @@ -4,170 +4,146 @@ 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`; +// Host passes serversPromise to CallToolPanel +interface HostProps { + serversPromise: Promise; } - -// 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() { - const [toolCalls, setToolCalls] = useState([]); +function Host({ serversPromise }: HostProps) { + 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 includes server selection with its own Suspense boundary +// CallToolPanel renders the unified form with Suspense around ServerSelect interface CallToolPanelProps { - addToolCall: (serverName: string, info: ToolCallInfo) => void; + serversPromise: Promise; + addToolCall: (info: ToolCallInfo) => void; } -function CallToolPanel({ addToolCall }: CallToolPanelProps) { - const [selectedServer, setSelectedServer] = useState(SERVERS[0]); - const [serverInfoPromise, setServerInfoPromise] = useState( - () => getServerInfo(selectedServer.port) - ); +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); + return true; + } catch { + return false; + } + }, [inputJson]); - const handleServerChange = (port: number) => { - const server = SERVERS.find(s => s.port === port) ?? SERVERS[0]; + const handleServerSelect = (server: ServerInfo) => { setSelectedServer(server); - setServerInfoPromise(getServerInfo(port)); + const [firstTool] = server.tools.keys(); + setSelectedTool(firstTool ?? ""); + }; + + const handleSubmit = () => { + if (!selectedServer) return; + const toolCallInfo = callTool(selectedServer, selectedTool, JSON.parse(inputJson)); + addToolCall(toolCallInfo); }; return (
- - - Connecting to {serverUrl(selectedServer.port)}...

}> - { e.preventDefault(); handleSubmit(); }}> + + +