hono-mcp-server 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +35 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/mcp.ts +61 -41
package/dist/index.mjs
CHANGED
|
@@ -73,28 +73,30 @@ function withCors(response) {
|
|
|
73
73
|
function mcp(app, options) {
|
|
74
74
|
const { mcpPath = "/mcp", codemode = false, loaderBinding = "LOADER" } = options;
|
|
75
75
|
const routes = extractRoutes(app);
|
|
76
|
-
const
|
|
76
|
+
const server = new McpServer({
|
|
77
77
|
name: options.name,
|
|
78
78
|
version: options.version,
|
|
79
79
|
...options.title && { title: options.title },
|
|
80
80
|
...options.description && { description: options.description }
|
|
81
|
-
};
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (!loader) return new Response(`Codemode requires ${loaderBinding} binding. Add worker_loaders to wrangler.jsonc.`, { status: 500 });
|
|
87
|
-
const server = createServer();
|
|
88
|
-
registerCodemodeTools(server, routes, app, loader);
|
|
89
|
-
const transport = new WebStandardStreamableHTTPServerTransport();
|
|
81
|
+
}, options.instructions ? { instructions: options.instructions } : codemode ? { instructions: "Use 'search' to find available endpoints, then 'execute' to run code against the API." } : void 0);
|
|
82
|
+
const transport = new WebStandardStreamableHTTPServerTransport();
|
|
83
|
+
if (codemode) {
|
|
84
|
+
let currentLoader;
|
|
85
|
+
registerCodemodeTools(server, routes, app, () => currentLoader);
|
|
90
86
|
server.connect(transport);
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
const handleMcp$1 = async (c) => {
|
|
88
|
+
if (c.req.method === "OPTIONS") return new Response(null, { headers: CORS_HEADERS });
|
|
89
|
+
currentLoader = c.env?.[loaderBinding];
|
|
90
|
+
return withCors(await transport.handleRequest(c.req.raw));
|
|
91
|
+
};
|
|
92
|
+
app.all(`${mcpPath}/*`, handleMcp$1);
|
|
93
|
+
app.all(mcpPath, handleMcp$1);
|
|
94
|
+
return app;
|
|
95
|
+
}
|
|
96
|
+
for (const route of routes) registerRouteAsTool(server, route, app);
|
|
97
|
+
server.connect(transport);
|
|
98
|
+
const handleMcp = async (c) => {
|
|
93
99
|
if (c.req.method === "OPTIONS") return new Response(null, { headers: CORS_HEADERS });
|
|
94
|
-
const server = createServer();
|
|
95
|
-
for (const route of routes) registerRouteAsTool(server, route, app);
|
|
96
|
-
const transport = new WebStandardStreamableHTTPServerTransport();
|
|
97
|
-
server.connect(transport);
|
|
98
100
|
return withCors(await transport.handleRequest(c.req.raw));
|
|
99
101
|
};
|
|
100
102
|
app.all(`${mcpPath}/*`, handleMcp);
|
|
@@ -293,7 +295,7 @@ async function executeCode(loader, code, app) {
|
|
|
293
295
|
};
|
|
294
296
|
return await worker.getEntrypoint().evaluate(fetch);
|
|
295
297
|
}
|
|
296
|
-
function registerCodemodeTools(server, routes, app,
|
|
298
|
+
function registerCodemodeTools(server, routes, app, getLoader) {
|
|
297
299
|
const apiSchema = routesToApiSchema(routes);
|
|
298
300
|
server.registerTool("search", {
|
|
299
301
|
description: `Search available API endpoints.
|
|
@@ -305,6 +307,14 @@ Example:
|
|
|
305
307
|
async () => endpoints.filter(e => e.path.includes('users'))`,
|
|
306
308
|
inputSchema: { code: z.string().describe("JavaScript async arrow function to search endpoints") }
|
|
307
309
|
}, async ({ code }) => {
|
|
310
|
+
const loader = getLoader();
|
|
311
|
+
if (!loader) return {
|
|
312
|
+
content: [{
|
|
313
|
+
type: "text",
|
|
314
|
+
text: "Error: LOADER binding not available"
|
|
315
|
+
}],
|
|
316
|
+
isError: true
|
|
317
|
+
};
|
|
308
318
|
const result = await executeSearch(loader, code, apiSchema);
|
|
309
319
|
if (result.error) return {
|
|
310
320
|
content: [{
|
|
@@ -333,6 +343,14 @@ Example:
|
|
|
333
343
|
async () => await fetch('/users')`,
|
|
334
344
|
inputSchema: { code: z.string().describe("JavaScript async arrow function to execute") }
|
|
335
345
|
}, async ({ code }) => {
|
|
346
|
+
const loader = getLoader();
|
|
347
|
+
if (!loader) return {
|
|
348
|
+
content: [{
|
|
349
|
+
type: "text",
|
|
350
|
+
text: "Error: LOADER binding not available"
|
|
351
|
+
}],
|
|
352
|
+
isError: true
|
|
353
|
+
};
|
|
336
354
|
const result = await executeCode(loader, code, app);
|
|
337
355
|
if (result.error) return {
|
|
338
356
|
content: [{
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["middleware"],"sources":["../src/mcp.ts"],"sourcesContent":["import { Hono } from \"hono\";\nimport type { Context, Env, Handler, MiddlewareHandler, ValidationTargets } from \"hono\";\nimport type { RouterRoute, H } from \"hono/types\";\nimport { validator } from \"hono/validator\";\nimport { z } from \"zod\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { ToolAnnotations } from \"@modelcontextprotocol/sdk/types.js\";\nimport { WebStandardStreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js\";\n\n// Configuration for registerTool()\nexport interface DescribeConfig<\n TInput extends z.ZodRawShape = z.ZodRawShape,\n TOutput extends z.ZodRawShape = z.ZodRawShape,\n> {\n description: string;\n inputSchema?: TInput;\n outputSchema?: TOutput;\n annotations?: ToolAnnotations;\n}\n\n// Internal metadata storage\ninterface ToolMetadata {\n description: string;\n inputSchema?: z.ZodRawShape;\n outputSchema?: z.ZodRawShape;\n annotations?: ToolAnnotations;\n}\n\nconst toolMetadata = new WeakMap<Function, ToolMetadata>();\n\n/**\n * Register a route as an MCP tool with description and optional schemas.\n * Always returns middleware - the actual handler should be a separate function.\n *\n * @example\n * ```ts\n * // Simple description\n * app.get('/users', registerTool('List all users'), (c) => c.json([]))\n *\n * // With config\n * app.get('/users', registerTool({ description: 'List all users' }), (c) => c.json([]))\n *\n * // With inputSchema - use c.req.valid('json') for typed input\n * app.post('/users',\n * registerTool({ description: 'Create a user', inputSchema: { name: z.string() } }),\n * async (c) => {\n * const { name } = c.req.valid('json') // typed!\n * return c.json({ id: 1, name })\n * }\n * )\n * ```\n */\n// Overload: string description\nexport function registerTool(\n description: string,\n): MiddlewareHandler;\n// Overload: config with inputSchema - provides typed validation\nexport function registerTool<TInput extends z.ZodRawShape, TOutput extends z.ZodRawShape = z.ZodRawShape>(\n config: DescribeConfig<TInput, TOutput> & { inputSchema: TInput },\n): MiddlewareHandler<Env, string, { in: { json: z.infer<z.ZodObject<TInput>> }; out: { json: z.infer<z.ZodObject<TInput>> } }>;\n// Overload: config without inputSchema\nexport function registerTool<TOutput extends z.ZodRawShape>(\n config: DescribeConfig<z.ZodRawShape, TOutput>,\n): MiddlewareHandler;\n// Implementation\nexport function registerTool(\n descriptionOrConfig: string | DescribeConfig,\n): MiddlewareHandler {\n const metadata: ToolMetadata =\n typeof descriptionOrConfig === \"string\"\n ? { description: descriptionOrConfig }\n : {\n description: descriptionOrConfig.description,\n inputSchema: descriptionOrConfig.inputSchema,\n outputSchema: descriptionOrConfig.outputSchema,\n annotations: descriptionOrConfig.annotations,\n };\n\n // If inputSchema is defined, return validating middleware\n if (metadata.inputSchema) {\n const schema = z.object(metadata.inputSchema);\n const middleware = validator(\"json\", (value) => {\n const result = schema.safeParse(value);\n if (!result.success) {\n throw new Error(result.error.message);\n }\n return result.data;\n });\n toolMetadata.set(middleware, metadata);\n return middleware;\n }\n\n // Otherwise return pass-through middleware that just stores metadata\n const middleware: MiddlewareHandler = async (_c, next) => {\n await next();\n };\n toolMetadata.set(middleware, metadata);\n return middleware;\n}\n\nfunction getToolMetadata(handler: unknown): ToolMetadata | undefined {\n if (typeof handler === \"function\") {\n return toolMetadata.get(handler);\n }\n return undefined;\n}\n\nfunction getDescription(handler: unknown): string | undefined {\n return getToolMetadata(handler)?.description;\n}\n\n// WorkerLoader interface (matches @cloudflare/workers-types)\ninterface WorkerLoader {\n get(\n id: string,\n factory: () => {\n compatibilityDate: string;\n compatibilityFlags?: string[];\n mainModule: string;\n modules: Record<string, string>;\n },\n ): { getEntrypoint(): { evaluate: (...args: unknown[]) => Promise<unknown> } };\n}\n\nexport interface McpOptions {\n name: string;\n version: string;\n title?: string;\n description?: string;\n instructions?: string;\n mcpPath?: string;\n /** Enable codemode - exposes search/execute tools instead of per-route tools. Requires LOADER binding. */\n codemode?: boolean;\n /** Binding name for Worker Loader (default: \"LOADER\"). Only used when codemode is true. */\n loaderBinding?: string;\n}\n\ntype HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n\ninterface Route {\n method: HttpMethod;\n path: string;\n description: string;\n handler: H;\n}\n\nconst CORS_HEADERS = {\n \"Access-Control-Allow-Origin\": \"*\",\n \"Access-Control-Allow-Methods\": \"GET, POST, DELETE, OPTIONS\",\n \"Access-Control-Allow-Headers\": \"Content-Type, Accept, mcp-session-id, mcp-protocol-version\",\n \"Access-Control-Expose-Headers\": \"mcp-session-id\",\n \"Access-Control-Max-Age\": \"86400\",\n};\n\nfunction withCors(response: Response): Response {\n const newHeaders = new Headers(response.headers);\n for (const [key, value] of Object.entries(CORS_HEADERS)) {\n newHeaders.set(key, value);\n }\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: newHeaders,\n });\n}\n\n/**\n * Wraps a Hono app to add an MCP endpoint that exposes routes as tools.\n *\n * @example\n * ```ts\n * import { mcp, describe } from 'hono-mcp'\n *\n * const app = new Hono()\n * .get('/users', describe('List all users', (c) => c.json([])))\n * .get('/users/:id', describe('Get user by ID', (c) => c.json({ id: c.req.param('id') })));\n *\n * export default mcp(app, { name: 'Users API', version: '1.0.0' });\n * ```\n *\n * @example Codemode (requires Worker Loader binding)\n * ```ts\n * export default mcp(app, { name: 'API', version: '1.0.0', codemode: true });\n * ```\n *\n */\nexport function mcp<E extends Env>(app: Hono<E>, options: McpOptions): Hono<E> {\n const { mcpPath = \"/mcp\", codemode = false, loaderBinding = \"LOADER\" } = options;\n const routes = extractRoutes(app);\n const serverInfo = {\n name: options.name,\n version: options.version,\n ...(options.title && { title: options.title }),\n ...(options.description && { description: options.description }),\n };\n\n const createServer = () =>\n new McpServer(\n serverInfo,\n options.instructions\n ? { instructions: options.instructions }\n : codemode\n ? {\n instructions:\n \"Use 'search' to find available endpoints, then 'execute' to run code against the API.\",\n }\n : undefined,\n );\n\n const handleMcp = codemode\n ? async (c: Context<E>) => {\n if (c.req.method === \"OPTIONS\") return new Response(null, { headers: CORS_HEADERS });\n\n const env = c.env as Record<string, unknown> | undefined;\n const loader = env?.[loaderBinding] as WorkerLoader | undefined;\n if (!loader) {\n return new Response(\n `Codemode requires ${loaderBinding} binding. Add worker_loaders to wrangler.jsonc.`,\n { status: 500 },\n );\n }\n\n const server = createServer();\n registerCodemodeTools(server, routes, app, loader);\n\n const transport = new WebStandardStreamableHTTPServerTransport();\n server.connect(transport);\n return withCors(await transport.handleRequest(c.req.raw));\n }\n : async (c: Context<E>) => {\n if (c.req.method === \"OPTIONS\") return new Response(null, { headers: CORS_HEADERS });\n\n const server = createServer();\n for (const route of routes) registerRouteAsTool(server, route, app);\n\n const transport = new WebStandardStreamableHTTPServerTransport();\n server.connect(transport);\n return withCors(await transport.handleRequest(c.req.raw));\n };\n\n app.all(`${mcpPath}/*`, handleMcp);\n app.all(mcpPath, handleMcp);\n\n return app;\n}\n\nfunction extractRoutes<E extends Env>(app: Hono<E>): Route[] {\n const routes: Route[] = [];\n const seen = new Set<string>();\n\n const honoRoutes: RouterRoute[] = app.routes;\n\n if (!honoRoutes) return routes;\n\n for (const route of honoRoutes) {\n const method = route.method.toUpperCase();\n if (![\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"].includes(method)) continue;\n if (route.path.startsWith(\"/mcp\")) continue;\n\n const key = `${method} ${route.path}`;\n if (seen.has(key)) continue;\n seen.add(key);\n\n const description = getDescription(route.handler) || generateDescription(method, route.path);\n routes.push({ method: method as HttpMethod, path: route.path, description, handler: route.handler });\n }\n\n return routes;\n}\n\nfunction generateDescription(method: string, path: string): string {\n const resource =\n path\n .split(\"/\")\n .filter((p) => p && !p.startsWith(\":\"))\n .pop() || \"resource\";\n const action =\n {\n GET: path.includes(\":\") ? \"Get\" : \"List\",\n POST: \"Create\",\n PUT: \"Update\",\n PATCH: \"Update\",\n DELETE: \"Delete\",\n }[method] || method;\n return `${action} ${resource}`;\n}\n\nfunction generateToolName(method: string, path: string): string {\n const cleanPath = path\n .replace(/^\\//, \"\")\n .replace(/\\/:([^/]+)/g, \"_by_$1\")\n .replace(/\\//g, \"_\")\n .replace(/[^a-zA-Z0-9_]/g, \"\");\n return `${method.toLowerCase()}_${cleanPath || \"root\"}`;\n}\n\nfunction extractPathParams(path: string): string[] {\n const matches = path.match(/:([^/]+)/g);\n return matches ? matches.map((m) => m.slice(1)) : [];\n}\n\nfunction registerRouteAsTool<E extends Env>(server: McpServer, route: Route, app: Hono<E>): void {\n const toolName = generateToolName(route.method, route.path);\n const pathParams = extractPathParams(route.path);\n const metadata = getToolMetadata(route.handler);\n\n // Use metadata.inputSchema if available, otherwise generate default schema\n let inputShape: Record<string, z.ZodType>;\n\n if (metadata?.inputSchema) {\n // Use the provided input schema from metadata\n inputShape = { ...metadata.inputSchema };\n // Still need to add path params if not already present\n for (const param of pathParams) {\n if (!(param in inputShape)) {\n inputShape[param] = z.string().describe(`Path parameter: ${param}`);\n }\n }\n } else {\n // Generate default schema\n inputShape = {};\n for (const param of pathParams) {\n inputShape[param] = z.string().describe(`Path parameter: ${param}`);\n }\n if ([\"POST\", \"PUT\", \"PATCH\"].includes(route.method)) {\n inputShape[\"body\"] = z.record(z.unknown()).optional().describe(\"Request body as JSON object\");\n }\n if ([\"GET\", \"DELETE\"].includes(route.method)) {\n inputShape[\"query\"] = z.record(z.string()).optional().describe(\"Query parameters\");\n }\n }\n\n server.registerTool(\n toolName,\n {\n description: `${route.description}\\n\\n${route.method} ${route.path}`,\n inputSchema: Object.keys(inputShape).length > 0 ? inputShape : undefined,\n ...(metadata?.outputSchema && { outputSchema: metadata.outputSchema }),\n ...(metadata?.annotations && { annotations: metadata.annotations }),\n },\n async (params: Record<string, unknown>) => {\n let url = route.path;\n for (const param of pathParams) {\n const value = params[param];\n if (value !== undefined) {\n url = url.replace(`:${param}`, encodeURIComponent(String(value)));\n }\n }\n\n const query = params[\"query\"] as Record<string, string> | undefined;\n if (query && Object.keys(query).length > 0) {\n url = `${url}?${new URLSearchParams(query).toString()}`;\n }\n\n const body = params[\"body\"] as Record<string, unknown> | undefined;\n const init: RequestInit = {\n method: route.method,\n headers: { \"Content-Type\": \"application/json\", Accept: \"application/json\" },\n };\n if (body && [\"POST\", \"PUT\", \"PATCH\"].includes(route.method)) {\n init.body = JSON.stringify(body);\n }\n\n try {\n const response = await app.fetch(new Request(`http://internal${url}`, init));\n const contentType = response.headers.get(\"content-type\") || \"\";\n const isJson = contentType.includes(\"application/json\");\n\n if (isJson) {\n const json = await response.json();\n // Return structured content if outputSchema is defined\n if (metadata?.outputSchema) {\n return {\n structuredContent: json,\n content: [{ type: \"text\" as const, text: JSON.stringify(json, null, 2) }],\n isError: !response.ok,\n };\n }\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(json, null, 2) }],\n isError: !response.ok,\n };\n }\n\n // Plain text response\n const text = await response.text();\n return { content: [{ type: \"text\" as const, text }], isError: !response.ok };\n } catch (error) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: `Error: ${error instanceof Error ? error.message : String(error)}`,\n },\n ],\n isError: true,\n };\n }\n },\n );\n}\n\n// Codemode: search and execute tools\ninterface ApiEndpoint {\n method: string;\n path: string;\n description: string;\n body?: string;\n}\n\nfunction routesToApiSchema(routes: Route[]): ApiEndpoint[] {\n return routes.map((route) => {\n const endpoint: ApiEndpoint = {\n method: route.method,\n path: route.path,\n description: route.description,\n };\n if ([\"POST\", \"PUT\", \"PATCH\"].includes(route.method)) {\n endpoint.body = \"JSON object\";\n }\n return endpoint;\n });\n}\n\nasync function executeSearch(\n loader: WorkerLoader,\n code: string,\n apiSchema: ApiEndpoint[],\n): Promise<{ result?: unknown; error?: string }> {\n const workerId = `search-${crypto.randomUUID()}`;\n\n const worker = loader.get(workerId, () => ({\n compatibilityDate: \"2026-01-14\",\n compatibilityFlags: [\"nodejs_compat\"],\n mainModule: \"worker.js\",\n modules: {\n \"worker.js\": `\n import { WorkerEntrypoint } from \"cloudflare:workers\";\n const endpoints = ${JSON.stringify(apiSchema)};\n export default class SearchExecutor extends WorkerEntrypoint {\n async evaluate() {\n try {\n const result = await (${code})();\n return { result };\n } catch (err) {\n return { error: err.message };\n }\n }\n }\n `,\n },\n }));\n\n const entrypoint = worker.getEntrypoint();\n return (await entrypoint.evaluate()) as { result?: unknown; error?: string };\n}\n\nasync function executeCode<E extends Env>(\n loader: WorkerLoader,\n code: string,\n app: Hono<E>,\n): Promise<{ result?: unknown; error?: string }> {\n const workerId = `execute-${crypto.randomUUID()}`;\n\n const worker = loader.get(workerId, () => ({\n compatibilityDate: \"2026-01-14\",\n compatibilityFlags: [\"nodejs_compat\"],\n mainModule: \"worker.js\",\n modules: {\n \"worker.js\": `\n import { WorkerEntrypoint } from \"cloudflare:workers\";\n\n export default class ExecuteWorker extends WorkerEntrypoint {\n async evaluate(fetch) {\n try {\n const result = await (${code})();\n return { result };\n } catch (err) {\n return { error: err.message };\n }\n }\n }\n `,\n },\n }));\n\n const fetch = async (path: string, options: RequestInit = {}) => {\n const response = await app.fetch(new Request(`http://internal${path}`, options));\n const contentType = response.headers.get(\"content-type\") || \"\";\n return contentType.includes(\"application/json\") ? response.json() : response.text();\n };\n\n const entrypoint = worker.getEntrypoint();\n return (await entrypoint.evaluate(fetch)) as { result?: unknown; error?: string };\n}\n\nfunction registerCodemodeTools<E extends Env>(\n server: McpServer,\n routes: Route[],\n app: Hono<E>,\n loader: WorkerLoader,\n): void {\n const apiSchema = routesToApiSchema(routes);\n\n // Search tool - discover available endpoints\n server.registerTool(\n \"search\",\n {\n description: `Search available API endpoints.\n\nAvailable in your code:\n const endpoints = [...] // Array of { method, path, description, body? }\n\nExample:\n async () => endpoints.filter(e => e.path.includes('users'))`,\n inputSchema: {\n code: z.string().describe(\"JavaScript async arrow function to search endpoints\"),\n },\n },\n async ({ code }) => {\n const result = await executeSearch(loader, code as string, apiSchema);\n if (result.error) {\n return {\n content: [{ type: \"text\" as const, text: `Error: ${result.error}` }],\n isError: true,\n };\n }\n return { content: [{ type: \"text\" as const, text: JSON.stringify(result.result, null, 2) }] };\n },\n );\n\n // Execute tool - run code against the API\n server.registerTool(\n \"execute\",\n {\n description: `Execute code against the API.\n\nTypes:\n interface RequestInit {\n method?: string;\n headers?: Record<string, string>;\n body?: string;\n }\n declare function fetch(path: string, options?: RequestInit): Promise<unknown>;\n\nExample:\n async () => await fetch('/users')`,\n inputSchema: { code: z.string().describe(\"JavaScript async arrow function to execute\") },\n },\n async ({ code }) => {\n const result = await executeCode(loader, code as string, app);\n if (result.error) {\n return {\n content: [{ type: \"text\" as const, text: `Error: ${result.error}` }],\n isError: true,\n };\n }\n return { content: [{ type: \"text\" as const, text: JSON.stringify(result.result, null, 2) }] };\n },\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["middleware","handleMcp"],"sources":["../src/mcp.ts"],"sourcesContent":["import { Hono } from \"hono\";\nimport type { Context, Env, Handler, MiddlewareHandler, ValidationTargets } from \"hono\";\nimport type { RouterRoute, H } from \"hono/types\";\nimport { validator } from \"hono/validator\";\nimport { z } from \"zod\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { ToolAnnotations } from \"@modelcontextprotocol/sdk/types.js\";\nimport { WebStandardStreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js\";\n\n// Configuration for registerTool()\nexport interface DescribeConfig<\n TInput extends z.ZodRawShape = z.ZodRawShape,\n TOutput extends z.ZodRawShape = z.ZodRawShape,\n> {\n description: string;\n inputSchema?: TInput;\n outputSchema?: TOutput;\n annotations?: ToolAnnotations;\n}\n\n// Internal metadata storage\ninterface ToolMetadata {\n description: string;\n inputSchema?: z.ZodRawShape;\n outputSchema?: z.ZodRawShape;\n annotations?: ToolAnnotations;\n}\n\nconst toolMetadata = new WeakMap<Function, ToolMetadata>();\n\n/**\n * Register a route as an MCP tool with description and optional schemas.\n * Always returns middleware - the actual handler should be a separate function.\n *\n * @example\n * ```ts\n * // Simple description\n * app.get('/users', registerTool('List all users'), (c) => c.json([]))\n *\n * // With config\n * app.get('/users', registerTool({ description: 'List all users' }), (c) => c.json([]))\n *\n * // With inputSchema - use c.req.valid('json') for typed input\n * app.post('/users',\n * registerTool({ description: 'Create a user', inputSchema: { name: z.string() } }),\n * async (c) => {\n * const { name } = c.req.valid('json') // typed!\n * return c.json({ id: 1, name })\n * }\n * )\n * ```\n */\n// Overload: string description\nexport function registerTool(\n description: string,\n): MiddlewareHandler;\n// Overload: config with inputSchema - provides typed validation\nexport function registerTool<TInput extends z.ZodRawShape, TOutput extends z.ZodRawShape = z.ZodRawShape>(\n config: DescribeConfig<TInput, TOutput> & { inputSchema: TInput },\n): MiddlewareHandler<Env, string, { in: { json: z.infer<z.ZodObject<TInput>> }; out: { json: z.infer<z.ZodObject<TInput>> } }>;\n// Overload: config without inputSchema\nexport function registerTool<TOutput extends z.ZodRawShape>(\n config: DescribeConfig<z.ZodRawShape, TOutput>,\n): MiddlewareHandler;\n// Implementation\nexport function registerTool(\n descriptionOrConfig: string | DescribeConfig,\n): MiddlewareHandler {\n const metadata: ToolMetadata =\n typeof descriptionOrConfig === \"string\"\n ? { description: descriptionOrConfig }\n : {\n description: descriptionOrConfig.description,\n inputSchema: descriptionOrConfig.inputSchema,\n outputSchema: descriptionOrConfig.outputSchema,\n annotations: descriptionOrConfig.annotations,\n };\n\n // If inputSchema is defined, return validating middleware\n if (metadata.inputSchema) {\n const schema = z.object(metadata.inputSchema);\n const middleware = validator(\"json\", (value) => {\n const result = schema.safeParse(value);\n if (!result.success) {\n throw new Error(result.error.message);\n }\n return result.data;\n });\n toolMetadata.set(middleware, metadata);\n return middleware;\n }\n\n // Otherwise return pass-through middleware that just stores metadata\n const middleware: MiddlewareHandler = async (_c, next) => {\n await next();\n };\n toolMetadata.set(middleware, metadata);\n return middleware;\n}\n\nfunction getToolMetadata(handler: unknown): ToolMetadata | undefined {\n if (typeof handler === \"function\") {\n return toolMetadata.get(handler);\n }\n return undefined;\n}\n\nfunction getDescription(handler: unknown): string | undefined {\n return getToolMetadata(handler)?.description;\n}\n\n// WorkerLoader interface (matches @cloudflare/workers-types)\ninterface WorkerLoader {\n get(\n id: string,\n factory: () => {\n compatibilityDate: string;\n compatibilityFlags?: string[];\n mainModule: string;\n modules: Record<string, string>;\n },\n ): { getEntrypoint(): { evaluate: (...args: unknown[]) => Promise<unknown> } };\n}\n\nexport interface McpOptions {\n name: string;\n version: string;\n title?: string;\n description?: string;\n instructions?: string;\n mcpPath?: string;\n /** Enable codemode - exposes search/execute tools instead of per-route tools. Requires LOADER binding. */\n codemode?: boolean;\n /** Binding name for Worker Loader (default: \"LOADER\"). Only used when codemode is true. */\n loaderBinding?: string;\n}\n\ntype HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n\ninterface Route {\n method: HttpMethod;\n path: string;\n description: string;\n handler: H;\n}\n\nconst CORS_HEADERS = {\n \"Access-Control-Allow-Origin\": \"*\",\n \"Access-Control-Allow-Methods\": \"GET, POST, DELETE, OPTIONS\",\n \"Access-Control-Allow-Headers\": \"Content-Type, Accept, mcp-session-id, mcp-protocol-version\",\n \"Access-Control-Expose-Headers\": \"mcp-session-id\",\n \"Access-Control-Max-Age\": \"86400\",\n};\n\nfunction withCors(response: Response): Response {\n const newHeaders = new Headers(response.headers);\n for (const [key, value] of Object.entries(CORS_HEADERS)) {\n newHeaders.set(key, value);\n }\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: newHeaders,\n });\n}\n\n/**\n * Wraps a Hono app to add an MCP endpoint that exposes routes as tools.\n *\n * @example\n * ```ts\n * import { mcp, describe } from 'hono-mcp'\n *\n * const app = new Hono()\n * .get('/users', describe('List all users', (c) => c.json([])))\n * .get('/users/:id', describe('Get user by ID', (c) => c.json({ id: c.req.param('id') })));\n *\n * export default mcp(app, { name: 'Users API', version: '1.0.0' });\n * ```\n *\n * @example Codemode (requires Worker Loader binding)\n * ```ts\n * export default mcp(app, { name: 'API', version: '1.0.0', codemode: true });\n * ```\n *\n */\nexport function mcp<E extends Env>(app: Hono<E>, options: McpOptions): Hono<E> {\n const { mcpPath = \"/mcp\", codemode = false, loaderBinding = \"LOADER\" } = options;\n const routes = extractRoutes(app);\n const serverInfo = {\n name: options.name,\n version: options.version,\n ...(options.title && { title: options.title }),\n ...(options.description && { description: options.description }),\n };\n\n const server = new McpServer(\n serverInfo,\n options.instructions\n ? { instructions: options.instructions }\n : codemode\n ? {\n instructions:\n \"Use 'search' to find available endpoints, then 'execute' to run code against the API.\",\n }\n : undefined,\n );\n\n const transport = new WebStandardStreamableHTTPServerTransport();\n\n // Register tools at startup\n if (codemode) {\n // For codemode, use a getter that captures the current request's loader\n let currentLoader: WorkerLoader | undefined;\n registerCodemodeTools(server, routes, app, () => currentLoader);\n\n // Connect server to transport\n server.connect(transport);\n\n const handleMcp = async (c: Context<E>) => {\n if (c.req.method === \"OPTIONS\") return new Response(null, { headers: CORS_HEADERS });\n\n // Capture loader from current request's env\n const env = c.env as Record<string, unknown> | undefined;\n currentLoader = env?.[loaderBinding] as WorkerLoader | undefined;\n\n return withCors(await transport.handleRequest(c.req.raw));\n };\n\n app.all(`${mcpPath}/*`, handleMcp);\n app.all(mcpPath, handleMcp);\n\n return app;\n }\n\n for (const route of routes) registerRouteAsTool(server, route, app);\n\n // Connect server to transport\n server.connect(transport);\n\n const handleMcp = async (c: Context<E>) => {\n if (c.req.method === \"OPTIONS\") return new Response(null, { headers: CORS_HEADERS });\n\n return withCors(await transport.handleRequest(c.req.raw));\n };\n\n app.all(`${mcpPath}/*`, handleMcp);\n app.all(mcpPath, handleMcp);\n\n return app;\n}\n\nfunction extractRoutes<E extends Env>(app: Hono<E>): Route[] {\n const routes: Route[] = [];\n const seen = new Set<string>();\n\n const honoRoutes: RouterRoute[] = app.routes;\n\n if (!honoRoutes) return routes;\n\n for (const route of honoRoutes) {\n const method = route.method.toUpperCase();\n if (![\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"].includes(method)) continue;\n if (route.path.startsWith(\"/mcp\")) continue;\n\n const key = `${method} ${route.path}`;\n if (seen.has(key)) continue;\n seen.add(key);\n\n const description = getDescription(route.handler) || generateDescription(method, route.path);\n routes.push({ method: method as HttpMethod, path: route.path, description, handler: route.handler });\n }\n\n return routes;\n}\n\nfunction generateDescription(method: string, path: string): string {\n const resource =\n path\n .split(\"/\")\n .filter((p) => p && !p.startsWith(\":\"))\n .pop() || \"resource\";\n const action =\n {\n GET: path.includes(\":\") ? \"Get\" : \"List\",\n POST: \"Create\",\n PUT: \"Update\",\n PATCH: \"Update\",\n DELETE: \"Delete\",\n }[method] || method;\n return `${action} ${resource}`;\n}\n\nfunction generateToolName(method: string, path: string): string {\n const cleanPath = path\n .replace(/^\\//, \"\")\n .replace(/\\/:([^/]+)/g, \"_by_$1\")\n .replace(/\\//g, \"_\")\n .replace(/[^a-zA-Z0-9_]/g, \"\");\n return `${method.toLowerCase()}_${cleanPath || \"root\"}`;\n}\n\nfunction extractPathParams(path: string): string[] {\n const matches = path.match(/:([^/]+)/g);\n return matches ? matches.map((m) => m.slice(1)) : [];\n}\n\nfunction registerRouteAsTool<E extends Env>(server: McpServer, route: Route, app: Hono<E>): void {\n const toolName = generateToolName(route.method, route.path);\n const pathParams = extractPathParams(route.path);\n const metadata = getToolMetadata(route.handler);\n\n // Use metadata.inputSchema if available, otherwise generate default schema\n let inputShape: Record<string, z.ZodType>;\n\n if (metadata?.inputSchema) {\n // Use the provided input schema from metadata\n inputShape = { ...metadata.inputSchema };\n // Still need to add path params if not already present\n for (const param of pathParams) {\n if (!(param in inputShape)) {\n inputShape[param] = z.string().describe(`Path parameter: ${param}`);\n }\n }\n } else {\n // Generate default schema\n inputShape = {};\n for (const param of pathParams) {\n inputShape[param] = z.string().describe(`Path parameter: ${param}`);\n }\n if ([\"POST\", \"PUT\", \"PATCH\"].includes(route.method)) {\n inputShape[\"body\"] = z.record(z.unknown()).optional().describe(\"Request body as JSON object\");\n }\n if ([\"GET\", \"DELETE\"].includes(route.method)) {\n inputShape[\"query\"] = z.record(z.string()).optional().describe(\"Query parameters\");\n }\n }\n\n server.registerTool(\n toolName,\n {\n description: `${route.description}\\n\\n${route.method} ${route.path}`,\n inputSchema: Object.keys(inputShape).length > 0 ? inputShape : undefined,\n ...(metadata?.outputSchema && { outputSchema: metadata.outputSchema }),\n ...(metadata?.annotations && { annotations: metadata.annotations }),\n },\n async (params: Record<string, unknown>) => {\n let url = route.path;\n for (const param of pathParams) {\n const value = params[param];\n if (value !== undefined) {\n url = url.replace(`:${param}`, encodeURIComponent(String(value)));\n }\n }\n\n const query = params[\"query\"] as Record<string, string> | undefined;\n if (query && Object.keys(query).length > 0) {\n url = `${url}?${new URLSearchParams(query).toString()}`;\n }\n\n const body = params[\"body\"] as Record<string, unknown> | undefined;\n const init: RequestInit = {\n method: route.method,\n headers: { \"Content-Type\": \"application/json\", Accept: \"application/json\" },\n };\n if (body && [\"POST\", \"PUT\", \"PATCH\"].includes(route.method)) {\n init.body = JSON.stringify(body);\n }\n\n try {\n const response = await app.fetch(new Request(`http://internal${url}`, init));\n const contentType = response.headers.get(\"content-type\") || \"\";\n const isJson = contentType.includes(\"application/json\");\n\n if (isJson) {\n const json = (await response.json()) as Record<string, unknown>;\n // Return structured content if outputSchema is defined\n if (metadata?.outputSchema) {\n return {\n structuredContent: json,\n content: [{ type: \"text\" as const, text: JSON.stringify(json, null, 2) }],\n isError: !response.ok,\n };\n }\n return {\n content: [{ type: \"text\" as const, text: JSON.stringify(json, null, 2) }],\n isError: !response.ok,\n };\n }\n\n // Plain text response\n const text = await response.text();\n return { content: [{ type: \"text\" as const, text }], isError: !response.ok };\n } catch (error) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: `Error: ${error instanceof Error ? error.message : String(error)}`,\n },\n ],\n isError: true,\n };\n }\n },\n );\n}\n\n// Codemode: search and execute tools\ninterface ApiEndpoint {\n method: string;\n path: string;\n description: string;\n body?: string;\n}\n\nfunction routesToApiSchema(routes: Route[]): ApiEndpoint[] {\n return routes.map((route) => {\n const endpoint: ApiEndpoint = {\n method: route.method,\n path: route.path,\n description: route.description,\n };\n if ([\"POST\", \"PUT\", \"PATCH\"].includes(route.method)) {\n endpoint.body = \"JSON object\";\n }\n return endpoint;\n });\n}\n\nasync function executeSearch(\n loader: WorkerLoader,\n code: string,\n apiSchema: ApiEndpoint[],\n): Promise<{ result?: unknown; error?: string }> {\n const workerId = `search-${crypto.randomUUID()}`;\n\n const worker = loader.get(workerId, () => ({\n compatibilityDate: \"2026-01-14\",\n compatibilityFlags: [\"nodejs_compat\"],\n mainModule: \"worker.js\",\n modules: {\n \"worker.js\": `\n import { WorkerEntrypoint } from \"cloudflare:workers\";\n const endpoints = ${JSON.stringify(apiSchema)};\n export default class SearchExecutor extends WorkerEntrypoint {\n async evaluate() {\n try {\n const result = await (${code})();\n return { result };\n } catch (err) {\n return { error: err.message };\n }\n }\n }\n `,\n },\n }));\n\n const entrypoint = worker.getEntrypoint();\n return (await entrypoint.evaluate()) as { result?: unknown; error?: string };\n}\n\nasync function executeCode<E extends Env>(\n loader: WorkerLoader,\n code: string,\n app: Hono<E>,\n): Promise<{ result?: unknown; error?: string }> {\n const workerId = `execute-${crypto.randomUUID()}`;\n\n const worker = loader.get(workerId, () => ({\n compatibilityDate: \"2026-01-14\",\n compatibilityFlags: [\"nodejs_compat\"],\n mainModule: \"worker.js\",\n modules: {\n \"worker.js\": `\n import { WorkerEntrypoint } from \"cloudflare:workers\";\n\n export default class ExecuteWorker extends WorkerEntrypoint {\n async evaluate(fetch) {\n try {\n const result = await (${code})();\n return { result };\n } catch (err) {\n return { error: err.message };\n }\n }\n }\n `,\n },\n }));\n\n const fetch = async (path: string, options: RequestInit = {}) => {\n const response = await app.fetch(new Request(`http://internal${path}`, options));\n const contentType = response.headers.get(\"content-type\") || \"\";\n return contentType.includes(\"application/json\") ? response.json() : response.text();\n };\n\n const entrypoint = worker.getEntrypoint();\n return (await entrypoint.evaluate(fetch)) as { result?: unknown; error?: string };\n}\n\nfunction registerCodemodeTools<E extends Env>(\n server: McpServer,\n routes: Route[],\n app: Hono<E>,\n getLoader: () => WorkerLoader | undefined,\n): void {\n const apiSchema = routesToApiSchema(routes);\n\n // Search tool - discover available endpoints\n server.registerTool(\n \"search\",\n {\n description: `Search available API endpoints.\n\nAvailable in your code:\n const endpoints = [...] // Array of { method, path, description, body? }\n\nExample:\n async () => endpoints.filter(e => e.path.includes('users'))`,\n inputSchema: {\n code: z.string().describe(\"JavaScript async arrow function to search endpoints\"),\n },\n },\n async ({ code }) => {\n const loader = getLoader();\n if (!loader) {\n return {\n content: [{ type: \"text\" as const, text: \"Error: LOADER binding not available\" }],\n isError: true,\n };\n }\n const result = await executeSearch(loader, code as string, apiSchema);\n if (result.error) {\n return {\n content: [{ type: \"text\" as const, text: `Error: ${result.error}` }],\n isError: true,\n };\n }\n return { content: [{ type: \"text\" as const, text: JSON.stringify(result.result, null, 2) }] };\n },\n );\n\n // Execute tool - run code against the API\n server.registerTool(\n \"execute\",\n {\n description: `Execute code against the API.\n\nTypes:\n interface RequestInit {\n method?: string;\n headers?: Record<string, string>;\n body?: string;\n }\n declare function fetch(path: string, options?: RequestInit): Promise<unknown>;\n\nExample:\n async () => await fetch('/users')`,\n inputSchema: { code: z.string().describe(\"JavaScript async arrow function to execute\") },\n },\n async ({ code }) => {\n const loader = getLoader();\n if (!loader) {\n return {\n content: [{ type: \"text\" as const, text: \"Error: LOADER binding not available\" }],\n isError: true,\n };\n }\n const result = await executeCode(loader, code as string, app);\n if (result.error) {\n return {\n content: [{ type: \"text\" as const, text: `Error: ${result.error}` }],\n isError: true,\n };\n }\n return { content: [{ type: \"text\" as const, text: JSON.stringify(result.result, null, 2) }] };\n },\n );\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/mcp.ts
CHANGED
|
@@ -194,49 +194,55 @@ export function mcp<E extends Env>(app: Hono<E>, options: McpOptions): Hono<E> {
|
|
|
194
194
|
...(options.description && { description: options.description }),
|
|
195
195
|
};
|
|
196
196
|
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
options.instructions
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
const handleMcp = codemode
|
|
211
|
-
? async (c: Context<E>) => {
|
|
212
|
-
if (c.req.method === "OPTIONS") return new Response(null, { headers: CORS_HEADERS });
|
|
213
|
-
|
|
214
|
-
const env = c.env as Record<string, unknown> | undefined;
|
|
215
|
-
const loader = env?.[loaderBinding] as WorkerLoader | undefined;
|
|
216
|
-
if (!loader) {
|
|
217
|
-
return new Response(
|
|
218
|
-
`Codemode requires ${loaderBinding} binding. Add worker_loaders to wrangler.jsonc.`,
|
|
219
|
-
{ status: 500 },
|
|
220
|
-
);
|
|
221
|
-
}
|
|
197
|
+
const server = new McpServer(
|
|
198
|
+
serverInfo,
|
|
199
|
+
options.instructions
|
|
200
|
+
? { instructions: options.instructions }
|
|
201
|
+
: codemode
|
|
202
|
+
? {
|
|
203
|
+
instructions:
|
|
204
|
+
"Use 'search' to find available endpoints, then 'execute' to run code against the API.",
|
|
205
|
+
}
|
|
206
|
+
: undefined,
|
|
207
|
+
);
|
|
222
208
|
|
|
223
|
-
|
|
224
|
-
registerCodemodeTools(server, routes, app, loader);
|
|
209
|
+
const transport = new WebStandardStreamableHTTPServerTransport();
|
|
225
210
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (c.req.method === "OPTIONS") return new Response(null, { headers: CORS_HEADERS });
|
|
211
|
+
// Register tools at startup
|
|
212
|
+
if (codemode) {
|
|
213
|
+
// For codemode, use a getter that captures the current request's loader
|
|
214
|
+
let currentLoader: WorkerLoader | undefined;
|
|
215
|
+
registerCodemodeTools(server, routes, app, () => currentLoader);
|
|
232
216
|
|
|
233
|
-
|
|
234
|
-
|
|
217
|
+
// Connect server to transport
|
|
218
|
+
server.connect(transport);
|
|
235
219
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
220
|
+
const handleMcp = async (c: Context<E>) => {
|
|
221
|
+
if (c.req.method === "OPTIONS") return new Response(null, { headers: CORS_HEADERS });
|
|
222
|
+
|
|
223
|
+
// Capture loader from current request's env
|
|
224
|
+
const env = c.env as Record<string, unknown> | undefined;
|
|
225
|
+
currentLoader = env?.[loaderBinding] as WorkerLoader | undefined;
|
|
226
|
+
|
|
227
|
+
return withCors(await transport.handleRequest(c.req.raw));
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
app.all(`${mcpPath}/*`, handleMcp);
|
|
231
|
+
app.all(mcpPath, handleMcp);
|
|
232
|
+
|
|
233
|
+
return app;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const route of routes) registerRouteAsTool(server, route, app);
|
|
237
|
+
|
|
238
|
+
// Connect server to transport
|
|
239
|
+
server.connect(transport);
|
|
240
|
+
|
|
241
|
+
const handleMcp = async (c: Context<E>) => {
|
|
242
|
+
if (c.req.method === "OPTIONS") return new Response(null, { headers: CORS_HEADERS });
|
|
243
|
+
|
|
244
|
+
return withCors(await transport.handleRequest(c.req.raw));
|
|
245
|
+
};
|
|
240
246
|
|
|
241
247
|
app.all(`${mcpPath}/*`, handleMcp);
|
|
242
248
|
app.all(mcpPath, handleMcp);
|
|
@@ -367,7 +373,7 @@ function registerRouteAsTool<E extends Env>(server: McpServer, route: Route, app
|
|
|
367
373
|
const isJson = contentType.includes("application/json");
|
|
368
374
|
|
|
369
375
|
if (isJson) {
|
|
370
|
-
const json = await response.json()
|
|
376
|
+
const json = (await response.json()) as Record<string, unknown>;
|
|
371
377
|
// Return structured content if outputSchema is defined
|
|
372
378
|
if (metadata?.outputSchema) {
|
|
373
379
|
return {
|
|
@@ -498,7 +504,7 @@ function registerCodemodeTools<E extends Env>(
|
|
|
498
504
|
server: McpServer,
|
|
499
505
|
routes: Route[],
|
|
500
506
|
app: Hono<E>,
|
|
501
|
-
|
|
507
|
+
getLoader: () => WorkerLoader | undefined,
|
|
502
508
|
): void {
|
|
503
509
|
const apiSchema = routesToApiSchema(routes);
|
|
504
510
|
|
|
@@ -518,6 +524,13 @@ Example:
|
|
|
518
524
|
},
|
|
519
525
|
},
|
|
520
526
|
async ({ code }) => {
|
|
527
|
+
const loader = getLoader();
|
|
528
|
+
if (!loader) {
|
|
529
|
+
return {
|
|
530
|
+
content: [{ type: "text" as const, text: "Error: LOADER binding not available" }],
|
|
531
|
+
isError: true,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
521
534
|
const result = await executeSearch(loader, code as string, apiSchema);
|
|
522
535
|
if (result.error) {
|
|
523
536
|
return {
|
|
@@ -548,6 +561,13 @@ Example:
|
|
|
548
561
|
inputSchema: { code: z.string().describe("JavaScript async arrow function to execute") },
|
|
549
562
|
},
|
|
550
563
|
async ({ code }) => {
|
|
564
|
+
const loader = getLoader();
|
|
565
|
+
if (!loader) {
|
|
566
|
+
return {
|
|
567
|
+
content: [{ type: "text" as const, text: "Error: LOADER binding not available" }],
|
|
568
|
+
isError: true,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
551
571
|
const result = await executeCode(loader, code as string, app);
|
|
552
572
|
if (result.error) {
|
|
553
573
|
return {
|