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 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 serverInfo = {
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 createServer = () => new McpServer(serverInfo, options.instructions ? { instructions: options.instructions } : codemode ? { instructions: "Use 'search' to find available endpoints, then 'execute' to run code against the API." } : void 0);
83
- const handleMcp = codemode ? async (c) => {
84
- if (c.req.method === "OPTIONS") return new Response(null, { headers: CORS_HEADERS });
85
- const loader = c.env?.[loaderBinding];
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
- return withCors(await transport.handleRequest(c.req.raw));
92
- } : async (c) => {
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, loader) {
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: [{
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono-mcp-server",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "hono app as mcp server",
5
5
  "keywords": [
6
6
  "api",
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 createServer = () =>
198
- new McpServer(
199
- serverInfo,
200
- options.instructions
201
- ? { instructions: options.instructions }
202
- : codemode
203
- ? {
204
- instructions:
205
- "Use 'search' to find available endpoints, then 'execute' to run code against the API.",
206
- }
207
- : undefined,
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
- const server = createServer();
224
- registerCodemodeTools(server, routes, app, loader);
209
+ const transport = new WebStandardStreamableHTTPServerTransport();
225
210
 
226
- const transport = new WebStandardStreamableHTTPServerTransport();
227
- server.connect(transport);
228
- return withCors(await transport.handleRequest(c.req.raw));
229
- }
230
- : async (c: Context<E>) => {
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
- const server = createServer();
234
- for (const route of routes) registerRouteAsTool(server, route, app);
217
+ // Connect server to transport
218
+ server.connect(transport);
235
219
 
236
- const transport = new WebStandardStreamableHTTPServerTransport();
237
- server.connect(transport);
238
- return withCors(await transport.handleRequest(c.req.raw));
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
- loader: WorkerLoader,
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 {