index.tsβ’6.36 kB
import { Hono } from "hono";
import { McpServer, StreamableHttpTransport } from "mcp-lite";
import { z } from "zod";
import { ToolRunner, getContainerBinding } from "./containers";
import { getContainer } from "@cloudflare/containers";
import type { Env, Tool, RegisterToolRequest } from "./types";
// Export Container class so it can be used as Durable Object
export { ToolRunner };
const app = new Hono<{ Bindings: Env }>();
// =============================================================================
// Tool Registration API
// =============================================================================
app.post("/api/register-tool", async (c) => {
try {
const body: RegisterToolRequest = await c.req.json();
// Validate required fields
if (!body.name || !body.description || !body.schema || !body.containerClass) {
return c.json(
{ error: "Missing required fields: name, description, schema, containerClass" },
400
);
}
// Since we only have one container now, container class is always "toolrunner"
// But we'll keep the field for backwards compatibility
const containerClass = body.containerClass?.toLowerCase() || "toolrunner";
// Insert tool into D1
const result = await c.env.DB.prepare(
`INSERT INTO tools (name, description, input_schema, container_class, instance_type, entrypoint)
VALUES (?, ?, ?, ?, ?, ?)`
)
.bind(
body.name,
body.description,
JSON.stringify(body.schema),
containerClass,
body.instanceType || "lite",
body.entrypoint || null
)
.run();
if (!result.success) {
return c.json({ error: "Failed to register tool", details: result.error }, 500);
}
return c.json({
success: true,
message: "Tool registered successfully",
toolName: body.name,
});
} catch (error: any) {
console.error("Error registering tool:", error);
return c.json(
{ error: "Failed to register tool", details: error.message },
500
);
}
});
// =============================================================================
// Tool Listing API (for debugging)
// =============================================================================
app.get("/api/tools", async (c) => {
try {
const { results } = await c.env.DB.prepare(
"SELECT * FROM tools ORDER BY created_at DESC"
).all<Tool>();
return c.json({
tools: results.map((tool) => ({
...tool,
input_schema: JSON.parse(tool.input_schema),
})),
});
} catch (error: any) {
console.error("Error listing tools:", error);
return c.json({ error: "Failed to list tools", details: error.message }, 500);
}
});
// =============================================================================
// MCP Endpoint with Dynamic Tool Discovery
// =============================================================================
app.all("/mcp", async (c) => {
const mcp = new McpServer({
name: "byob-mcp-server",
version: "1.0.0",
schemaAdapter: (schema) => z.toJSONSchema(schema as z.ZodType),
});
// Fetch all tools from D1
const { results: tools } = await c.env.DB.prepare(
"SELECT * FROM tools"
).all<Tool>();
console.log(`Loaded ${tools.length} tools from D1`);
// Dynamically register each tool with the MCP server
for (const tool of tools) {
const inputSchema = JSON.parse(tool.input_schema);
// Convert JSON Schema to Zod schema
// For simplicity, we'll use z.any() and validate later
// In production, you'd want proper JSON Schema -> Zod conversion
const zodSchema = z.object(
Object.keys(inputSchema.properties || {}).reduce((acc, key) => {
acc[key] = z.any();
return acc;
}, {} as Record<string, z.ZodAny>)
);
mcp.tool(tool.name, {
description: tool.description,
inputSchema: zodSchema,
handler: async (args) => {
try {
console.log(`Invoking tool: ${tool.name} with args:`, args);
// Get the container binding (we only have one now)
const containerBinding = getContainerBinding(c.env);
// Get or create container instance
// Use a consistent name for the container so we can reuse it
const containerName = "toolrunner-instance";
const container = getContainer(containerBinding, containerName);
// Call the container's /execute endpoint
const response = await container.fetch("http://container/execute", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(args),
});
if (!response.ok) {
const errorText = await response.text();
return {
content: [
{
type: "text",
text: `Container error: ${response.status} ${response.statusText}\n${errorText}`,
},
],
isError: true,
};
}
const result = await response.json();
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error: any) {
console.error(`Error invoking tool ${tool.name}:`, error);
return {
content: [
{
type: "text",
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
},
});
}
const transport = new StreamableHttpTransport();
const httpHandler = transport.bind(mcp);
const response = await httpHandler(c.req.raw);
return response;
});
// =============================================================================
// Health Check
// =============================================================================
app.get("/", (c) => {
return c.json({
name: "BYOB MCP Server",
endpoints: {
mcp: "/mcp",
registerTool: "POST /api/register-tool",
listTools: "GET /api/tools",
},
containerInfo: {
single_universal_container: true,
supports: ["echo", "uppercase", "jq"]
}
});
});
export default app;