Add dynamic example orchestration script
Replace hardcoded port assignments and npm scripts with run-all.ts that: - Auto-discovers example directories and assigns ports - Passes server URLs to basic-host via SERVERS env var - Supports start, dev, and build commands Update basic-host to fetch server list from /api/servers endpoint and display server names from MCP server metadata. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2b53efda5a
commit
8bff1dff68
3 changed files with 180 additions and 87 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export const log = {
|
|||
|
||||
|
||||
export interface ServerInfo {
|
||||
name: string;
|
||||
client: Client;
|
||||
tools: Map<string, Tool>;
|
||||
appHtmlCache: Map<string, string>;
|
||||
|
|
@ -30,11 +31,13 @@ export async function connectToServer(serverUrl: URL): Promise<ServerInfo> {
|
|||
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() };
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,53 +4,31 @@ 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`;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
// 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() {
|
||||
// Host receives connected servers via promise, uses single use() call
|
||||
interface HostProps {
|
||||
serversPromise: Promise<ServerInfo[]>;
|
||||
}
|
||||
function Host({ serversPromise }: HostProps) {
|
||||
const servers = use(serversPromise);
|
||||
const [toolCalls, setToolCalls] = useState<ToolCallEntry[]>([]);
|
||||
|
||||
if (servers.length === 0) {
|
||||
return <p>No servers configured. Set SERVERS environment variable.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{toolCalls.map((entry, i) => (
|
||||
<ToolCallInfoPanel key={i} serverName={entry.serverName} toolCallInfo={entry.info} />
|
||||
))}
|
||||
<CallToolPanel
|
||||
servers={servers}
|
||||
addToolCall={(serverName, info) => setToolCalls([...toolCalls, { serverName, info }])}
|
||||
/>
|
||||
</>
|
||||
|
|
@ -58,60 +36,48 @@ function Host() {
|
|||
}
|
||||
|
||||
|
||||
// CallToolPanel includes server selection with its own Suspense boundary
|
||||
// CallToolPanel manages server selection from already-connected servers
|
||||
interface CallToolPanelProps {
|
||||
servers: ServerInfo[];
|
||||
addToolCall: (serverName: string, info: ToolCallInfo) => void;
|
||||
}
|
||||
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));
|
||||
};
|
||||
function CallToolPanel({ servers, addToolCall }: CallToolPanelProps) {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const selectedServer = servers[selectedIndex];
|
||||
|
||||
return (
|
||||
<div className={styles.callToolPanel}>
|
||||
<label>
|
||||
Server
|
||||
<select
|
||||
value={selectedServer.port}
|
||||
onChange={(e) => handleServerChange(Number(e.target.value))}
|
||||
value={selectedIndex}
|
||||
onChange={(e) => setSelectedIndex(Number(e.target.value))}
|
||||
>
|
||||
{SERVERS.map(({ name, port }) => (
|
||||
<option key={port} value={port}>
|
||||
{name} (:{port})
|
||||
{servers.map((server, i) => (
|
||||
<option key={i} value={i}>
|
||||
{server.name}
|
||||
</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>
|
||||
<ToolCallForm
|
||||
key={selectedIndex}
|
||||
serverName={selectedServer.name}
|
||||
serverInfo={selectedServer}
|
||||
addToolCall={addToolCall}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// ToolCallForm renders inside Suspense - needs serverInfo for tool list
|
||||
// ToolCallForm receives already-resolved serverInfo
|
||||
interface ToolCallFormProps {
|
||||
serverName: string;
|
||||
serverInfoPromise: Promise<ServerInfo>;
|
||||
serverInfo: ServerInfo;
|
||||
addToolCall: (serverName: string, info: ToolCallInfo) => void;
|
||||
}
|
||||
function ToolCallForm({ serverName, serverInfoPromise, addToolCall }: ToolCallFormProps) {
|
||||
const serverInfo = use(serverInfoPromise);
|
||||
function ToolCallForm({ serverName, serverInfo, addToolCall }: ToolCallFormProps) {
|
||||
const toolNames = Array.from(serverInfo.tools.keys());
|
||||
const [selectedTool, setSelectedTool] = useState(toolNames[0] ?? "");
|
||||
const [inputJson, setInputJson] = useState("{}");
|
||||
|
|
@ -264,8 +230,18 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|||
}
|
||||
|
||||
|
||||
async function connectToAllServers(): Promise<ServerInfo[]> {
|
||||
const serverUrlsResponse = await fetch("/api/servers");
|
||||
const serverUrls = (await serverUrlsResponse.json()) as string[];
|
||||
return Promise.all(serverUrls.map((url) => connectToServer(new URL(url))));
|
||||
}
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<Host />
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<p className={styles.connecting}>Connecting to servers...</p>}>
|
||||
<Host serversPromise={connectToAllServers()} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue