Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions examples/basic-host/src/index.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
.connecting {
padding: 1rem 0;
text-align: center;
color: #666;
}

.callToolPanel, .toolCallInfoPanel {
margin: 0 auto;
padding: 1rem;
Expand All @@ -10,6 +16,9 @@
}

.callToolPanel {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 480px;

form {
Expand Down
165 changes: 120 additions & 45 deletions examples/basic-host/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,113 @@ import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy,
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 {
return `http://localhost:${port}/mcp`;
}

interface HostProps {
serverInfoPromise: Promise<ServerInfo>;
// Cache server connections to avoid reconnecting when switching between servers
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;
}
function Host({ serverInfoPromise }: HostProps) {
const serverInfo = use(serverInfoPromise);
const [toolCallInfos, setToolCallInfos] = useState<ToolCallInfo[]>([]);


// 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 (
<>
{toolCallInfos.map((info, i) => (
<ToolCallInfoPanel key={i} toolCallInfo={info} />
{toolCalls.map((entry, i) => (
<ToolCallInfoPanel key={i} serverName={entry.serverName} toolCallInfo={entry.info} />
))}
<CallToolPanel
serverInfo={serverInfo}
addToolCallInfo={(info) => setToolCallInfos([...toolCallInfos, info])}
addToolCall={(serverName, info) => setToolCalls([...toolCalls, { serverName, info }])}
/>
</>
);
}


// CallToolPanel includes server selection with its own Suspense boundary
interface CallToolPanelProps {
serverInfo: ServerInfo;
addToolCallInfo: (toolCallInfo: ToolCallInfo) => void;
addToolCall: (serverName: string, info: 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 [selectedTool, setSelectedTool] = useState(toolNames[0] ?? "");
const [inputJson, setInputJson] = useState("{}");
Expand All @@ -48,48 +126,47 @@ function CallToolPanel({ serverInfo, addToolCallInfo }: CallToolPanelProps) {

const handleSubmit = () => {
const toolCallInfo = callTool(serverInfo, selectedTool, JSON.parse(inputJson));
addToolCallInfo(toolCallInfo);
addToolCall(serverName, toolCallInfo);
};

return (
<div className={styles.callToolPanel}>
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<label>
Tool Name
<select
value={selectedTool}
onChange={(e) => setSelectedTool(e.target.value)}
>
{toolNames.map((name) => (
<option key={name} value={name}>{name}</option>
))}
</select>
</label>
<label>
Tool Input
<textarea
aria-invalid={!isValidJson}
value={inputJson}
onChange={(e) => setInputJson(e.target.value)}
/>
</label>
<button type="submit" disabled={!selectedTool || !isValidJson}>
Call Tool
</button>
</form>
</div>
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<label>
Tool
<select
value={selectedTool}
onChange={(e) => setSelectedTool(e.target.value)}
>
{toolNames.map((name) => (
<option key={name} value={name}>{name}</option>
))}
</select>
</label>
<label>
Input
<textarea
aria-invalid={!isValidJson}
value={inputJson}
onChange={(e) => setInputJson(e.target.value)}
/>
</label>
<button type="submit" disabled={!selectedTool || !isValidJson}>
Call Tool
</button>
</form>
);
}


interface ToolCallInfoPanelProps {
serverName: string;
toolCallInfo: ToolCallInfo;
}
function ToolCallInfoPanel({ toolCallInfo }: ToolCallInfoPanelProps) {
function ToolCallInfoPanel({ serverName, toolCallInfo }: ToolCallInfoPanelProps) {
return (
<div className={styles.toolCallInfoPanel}>
<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} />
</div>
<div className={styles.outputInfoPanel}>
Expand Down Expand Up @@ -188,8 +265,6 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {

createRoot(document.getElementById("root")!).render(
<StrictMode>
<Suspense fallback={<p>Connecting to server ({MCP_SERVER_URL.href})...</p>}>
<Host serverInfoPromise={connectToServer(MCP_SERVER_URL)} />
</Suspense>
<Host />
</StrictMode>,
);
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,15 @@
"build:all": "npm run build && npm run examples:build",
"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: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-server-react": "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-react": "PORT=3101 npm run --workspace=examples/basic-server-react 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'",
"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",
Expand Down
Loading