hono-mcp-server 0.0.1 → 0.0.2

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/README.md CHANGED
@@ -6,20 +6,25 @@ Expose your [Hono](https://hono.dev) API endpoints as [MCP](https://modelcontext
6
6
 
7
7
  ```ts
8
8
  import { Hono } from "hono";
9
- import { mcp, describe } from "hono-mcp-server";
9
+ import { z } from "zod";
10
+ import { mcp, registerTool } from "hono-mcp-server";
10
11
 
11
12
  const app = new Hono()
12
- .get(
13
- "/users",
14
- describe("List all users", (c) => c.json([{ id: 1, name: "Alice" }])),
15
- )
16
- .get(
17
- "/users/:id",
18
- describe("Get user by ID", (c) => c.json({ id: c.req.param("id") })),
19
- )
13
+ .get("/users", registerTool("List all users"), (c) => c.json([{ id: 1, name: "Alice" }]))
14
+ .get("/users/:id", registerTool("Get user by ID"), (c) => c.json({ id: c.req.param("id") }))
20
15
  .post(
21
16
  "/users",
22
- describe("Create a user", async (c) => c.json(await c.req.json(), 201)),
17
+ registerTool({
18
+ description: "Create a new user",
19
+ inputSchema: {
20
+ name: z.string().describe("User's full name"),
21
+ email: z.string().email().describe("User's email address"),
22
+ },
23
+ }),
24
+ async (c) => {
25
+ const { name } = c.req.valid("json"); // typed!
26
+ return c.json({ id: 1, name });
27
+ },
23
28
  );
24
29
 
25
30
  export default mcp(app, {
@@ -30,6 +35,35 @@ export default mcp(app, {
30
35
 
31
36
  This adds an `/mcp` endpoint that exposes your routes as MCP tools.
32
37
 
38
+ ## Input & Output Schemas
39
+
40
+ Use `registerTool()` with `inputSchema` for validated, typed input. Access validated data with `c.req.valid('json')`:
41
+
42
+ ```ts
43
+ import { z } from "zod";
44
+ import { registerTool } from "hono-mcp-server";
45
+
46
+ app.post(
47
+ "/search",
48
+ registerTool({
49
+ description: "Search for items",
50
+ inputSchema: {
51
+ query: z.string().describe("Search query"),
52
+ limit: z.number().optional().describe("Max results"),
53
+ },
54
+ outputSchema: {
55
+ results: z.array(z.object({ id: z.string(), title: z.string() })),
56
+ },
57
+ }),
58
+ async (c) => {
59
+ const { query, limit } = c.req.valid("json"); // typed!
60
+ return c.json({ results: [] });
61
+ },
62
+ );
63
+ ```
64
+
65
+ When `outputSchema` is defined, the tool returns structured content that MCP clients can parse.
66
+
33
67
  ## Options
34
68
 
35
69
  ```ts
package/dist/index.d.mts CHANGED
@@ -1,15 +1,48 @@
1
- import { Env, Handler, Hono } from "hono";
1
+ import { z } from "zod";
2
+ import { Env, Hono, MiddlewareHandler } from "hono";
3
+ import { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
2
4
 
3
5
  //#region src/mcp.d.ts
6
+ interface DescribeConfig<TInput extends z.ZodRawShape = z.ZodRawShape, TOutput extends z.ZodRawShape = z.ZodRawShape> {
7
+ description: string;
8
+ inputSchema?: TInput;
9
+ outputSchema?: TOutput;
10
+ annotations?: ToolAnnotations;
11
+ }
4
12
  /**
5
- * Add a description to a route handler for MCP tool generation.
13
+ * Register a route as an MCP tool with description and optional schemas.
14
+ * Always returns middleware - the actual handler should be a separate function.
6
15
  *
7
16
  * @example
8
17
  * ```ts
9
- * app.get('/users', describe('List all users', (c) => c.json([])))
18
+ * // Simple description
19
+ * app.get('/users', registerTool('List all users'), (c) => c.json([]))
20
+ *
21
+ * // With config
22
+ * app.get('/users', registerTool({ description: 'List all users' }), (c) => c.json([]))
23
+ *
24
+ * // With inputSchema - use c.req.valid('json') for typed input
25
+ * app.post('/users',
26
+ * registerTool({ description: 'Create a user', inputSchema: { name: z.string() } }),
27
+ * async (c) => {
28
+ * const { name } = c.req.valid('json') // typed!
29
+ * return c.json({ id: 1, name })
30
+ * }
31
+ * )
10
32
  * ```
11
33
  */
12
- declare function describe<H extends Handler>(description: string, handler: H): H;
34
+ declare function registerTool(description: string): MiddlewareHandler;
35
+ declare function registerTool<TInput extends z.ZodRawShape, TOutput extends z.ZodRawShape = z.ZodRawShape>(config: DescribeConfig<TInput, TOutput> & {
36
+ inputSchema: TInput;
37
+ }): MiddlewareHandler<Env, string, {
38
+ in: {
39
+ json: z.infer<z.ZodObject<TInput>>;
40
+ };
41
+ out: {
42
+ json: z.infer<z.ZodObject<TInput>>;
43
+ };
44
+ }>;
45
+ declare function registerTool<TOutput extends z.ZodRawShape>(config: DescribeConfig<z.ZodRawShape, TOutput>): MiddlewareHandler;
13
46
  interface McpOptions {
14
47
  name: string;
15
48
  version: string;
@@ -44,5 +77,5 @@ interface McpOptions {
44
77
  */
45
78
  declare function mcp<E extends Env>(app: Hono<E>, options: McpOptions): Hono<E>;
46
79
  //#endregion
47
- export { type McpOptions, describe, mcp };
80
+ export { type DescribeConfig, type McpOptions, mcp, registerTool };
48
81
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/mcp.ts"],"mappings":";;;;AAgBA;;;;;AAsBA;AA6DA;iBAnFgB,QAAA,WAAmB,OAAA,CAAA,CAAA,WAAA,UAAA,OAAA,EAAuC,CAAA,GAAI,CAAA;AAAA,UAsB7D,UAAA;EAAA,IAAA;EAAA,OAAA;EAAA,KAAA;EAAA,WAAA;EAAA,YAAA;EAAA,OAAA;EAAA;EAAA,QAAA;EAAA;EAAA,aAAA;AAAA;AAAA;AA6DjB;;;;;;;;;;;;;;;;;;;AA7DiB,iBA6DD,GAAA,WAAc,GAAA,CAAA,CAAA,GAAA,EAAU,IAAA,CAAK,CAAA,GAAA,OAAA,EAAa,UAAA,GAAa,IAAA,CAAK,CAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/mcp.ts"],"mappings":";;;;;UAUiB,cAAA,gBACA,CAAA,CAAE,WAAA,GAAc,CAAA,CAAE,WAAA,kBACjB,CAAA,CAAE,WAAA,GAAc,CAAA,CAAE,WAAA;EAAA,WAAA;EAAA,WAAA,GAGpB,MAAA;EAAA,YAAA,GACC,OAAA;EAAA,WAAA,GACD,eAAA;AAAA;AAAA;;AAoChB;AAIA;;;;;;;;;;;;;;;;;AAIA;;AA5CgB,iBAoCA,YAAA,CAAA,WAAA,WAEb,iBAAA;AAAA,iBAEa,YAAA,gBAA4B,CAAA,CAAE,WAAA,kBAA6B,CAAA,CAAE,WAAA,GAAc,CAAA,CAAE,WAAA,CAAA,CAAA,MAAA,EACnF,cAAA,CAAe,MAAA,EAAQ,OAAA;EAAA,WAAA,EAA0B,MAAA;AAAA,IACxD,iBAAA,CAAkB,GAAA;EAAA,EAAA;IAAA,IAAA,EAA2B,CAAA,CAAE,KAAA,CAAM,CAAA,CAAE,SAAA,CAAU,MAAA;EAAA;EAAA,GAAA;IAAA,IAAA,EAAyB,CAAA,CAAE,KAAA,CAAM,CAAA,CAAE,SAAA,CAAU,MAAA;EAAA;AAAA;AAAA,iBAEjG,YAAA,iBAA6B,CAAA,CAAE,WAAA,CAAA,CAAA,MAAA,EACrC,cAAA,CAAe,CAAA,CAAE,WAAA,EAAa,OAAA,IACrC,iBAAA;AAAA,UA6Dc,UAAA;EAAA,IAAA;EAAA,OAAA;EAAA,KAAA;EAAA,WAAA;EAAA,YAAA;EAAA,OAAA;EAAA;EAAA,QAAA;EAAA;EAAA,aAAA;AAAA;AAAA;AA8DjB;;;;;;;;;;;;;;;;;;;AA9DiB,iBA8DD,GAAA,WAAc,GAAA,CAAA,CAAA,GAAA,EAAU,IAAA,CAAK,CAAA,GAAA,OAAA,EAAa,UAAA,GAAa,IAAA,CAAK,CAAA"}
package/dist/index.mjs CHANGED
@@ -1,23 +1,38 @@
1
+ import { validator } from "hono/validator";
1
2
  import { z } from "zod";
2
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
4
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
4
5
 
5
6
  //#region src/mcp.ts
6
- const MCP_DESCRIPTION = Symbol("mcp_description");
7
- /**
8
- * Add a description to a route handler for MCP tool generation.
9
- *
10
- * @example
11
- * ```ts
12
- * app.get('/users', describe('List all users', (c) => c.json([])))
13
- * ```
14
- */
15
- function describe(description, handler) {
16
- handler[MCP_DESCRIPTION] = description;
17
- return handler;
7
+ const toolMetadata = /* @__PURE__ */ new WeakMap();
8
+ function registerTool(descriptionOrConfig) {
9
+ const metadata = typeof descriptionOrConfig === "string" ? { description: descriptionOrConfig } : {
10
+ description: descriptionOrConfig.description,
11
+ inputSchema: descriptionOrConfig.inputSchema,
12
+ outputSchema: descriptionOrConfig.outputSchema,
13
+ annotations: descriptionOrConfig.annotations
14
+ };
15
+ if (metadata.inputSchema) {
16
+ const schema = z.object(metadata.inputSchema);
17
+ const middleware$1 = validator("json", (value) => {
18
+ const result = schema.safeParse(value);
19
+ if (!result.success) throw new Error(result.error.message);
20
+ return result.data;
21
+ });
22
+ toolMetadata.set(middleware$1, metadata);
23
+ return middleware$1;
24
+ }
25
+ const middleware = async (_c, next) => {
26
+ await next();
27
+ };
28
+ toolMetadata.set(middleware, metadata);
29
+ return middleware;
30
+ }
31
+ function getToolMetadata(handler) {
32
+ if (typeof handler === "function") return toolMetadata.get(handler);
18
33
  }
19
34
  function getDescription(handler) {
20
- return handler?.[MCP_DESCRIPTION];
35
+ return getToolMetadata(handler)?.description;
21
36
  }
22
37
  const CORS_HEADERS = {
23
38
  "Access-Control-Allow-Origin": "*",
@@ -77,7 +92,7 @@ function mcp(app, options) {
77
92
  } : async (c) => {
78
93
  if (c.req.method === "OPTIONS") return new Response(null, { headers: CORS_HEADERS });
79
94
  const server = createServer();
80
- for (const route of routes) registerTool(server, route, app);
95
+ for (const route of routes) registerRouteAsTool(server, route, app);
81
96
  const transport = new WebStandardStreamableHTTPServerTransport();
82
97
  server.connect(transport);
83
98
  return withCors(await transport.handleRequest(c.req.raw));
@@ -108,7 +123,8 @@ function extractRoutes(app) {
108
123
  routes.push({
109
124
  method,
110
125
  path: route.path,
111
- description
126
+ description,
127
+ handler: route.handler
112
128
  });
113
129
  }
114
130
  return routes;
@@ -131,20 +147,29 @@ function extractPathParams(path) {
131
147
  const matches = path.match(/:([^/]+)/g);
132
148
  return matches ? matches.map((m) => m.slice(1)) : [];
133
149
  }
134
- function registerTool(server, route, app) {
150
+ function registerRouteAsTool(server, route, app) {
135
151
  const toolName = generateToolName(route.method, route.path);
136
152
  const pathParams = extractPathParams(route.path);
137
- const inputShape = {};
138
- for (const param of pathParams) inputShape[param] = z.string().describe(`Path parameter: ${param}`);
139
- if ([
140
- "POST",
141
- "PUT",
142
- "PATCH"
143
- ].includes(route.method)) inputShape["body"] = z.record(z.unknown()).optional().describe("Request body as JSON object");
144
- if (["GET", "DELETE"].includes(route.method)) inputShape["query"] = z.record(z.string()).optional().describe("Query parameters");
153
+ const metadata = getToolMetadata(route.handler);
154
+ let inputShape;
155
+ if (metadata?.inputSchema) {
156
+ inputShape = { ...metadata.inputSchema };
157
+ for (const param of pathParams) if (!(param in inputShape)) inputShape[param] = z.string().describe(`Path parameter: ${param}`);
158
+ } else {
159
+ inputShape = {};
160
+ for (const param of pathParams) inputShape[param] = z.string().describe(`Path parameter: ${param}`);
161
+ if ([
162
+ "POST",
163
+ "PUT",
164
+ "PATCH"
165
+ ].includes(route.method)) inputShape["body"] = z.record(z.unknown()).optional().describe("Request body as JSON object");
166
+ if (["GET", "DELETE"].includes(route.method)) inputShape["query"] = z.record(z.string()).optional().describe("Query parameters");
167
+ }
145
168
  server.registerTool(toolName, {
146
169
  description: `${route.description}\n\n${route.method} ${route.path}`,
147
- inputSchema: Object.keys(inputShape).length > 0 ? inputShape : void 0
170
+ inputSchema: Object.keys(inputShape).length > 0 ? inputShape : void 0,
171
+ ...metadata?.outputSchema && { outputSchema: metadata.outputSchema },
172
+ ...metadata?.annotations && { annotations: metadata.annotations }
148
173
  }, async (params) => {
149
174
  let url = route.path;
150
175
  for (const param of pathParams) {
@@ -168,10 +193,28 @@ function registerTool(server, route, app) {
168
193
  ].includes(route.method)) init.body = JSON.stringify(body);
169
194
  try {
170
195
  const response = await app.fetch(new Request(`http://internal${url}`, init));
196
+ if ((response.headers.get("content-type") || "").includes("application/json")) {
197
+ const json = await response.json();
198
+ if (metadata?.outputSchema) return {
199
+ structuredContent: json,
200
+ content: [{
201
+ type: "text",
202
+ text: JSON.stringify(json, null, 2)
203
+ }],
204
+ isError: !response.ok
205
+ };
206
+ return {
207
+ content: [{
208
+ type: "text",
209
+ text: JSON.stringify(json, null, 2)
210
+ }],
211
+ isError: !response.ok
212
+ };
213
+ }
171
214
  return {
172
215
  content: [{
173
216
  type: "text",
174
- text: (response.headers.get("content-type") || "").includes("application/json") ? JSON.stringify(await response.json(), null, 2) : await response.text()
217
+ text: await response.text()
175
218
  }],
176
219
  isError: !response.ok
177
220
  };
@@ -306,5 +349,5 @@ Example:
306
349
  }
307
350
 
308
351
  //#endregion
309
- export { describe, mcp };
352
+ export { mcp, registerTool };
310
353
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/mcp.ts"],"sourcesContent":["import { Hono } from \"hono\";\nimport type { Env, Handler } from \"hono\";\nimport { z } from \"zod\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { WebStandardStreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js\";\n\nconst MCP_DESCRIPTION = Symbol(\"mcp_description\");\n\n/**\n * Add a description to a route handler for MCP tool generation.\n *\n * @example\n * ```ts\n * app.get('/users', describe('List all users', (c) => c.json([])))\n * ```\n */\nexport function describe<H extends Handler>(description: string, handler: H): H {\n (handler as any)[MCP_DESCRIPTION] = description;\n return handler;\n}\n\nfunction getDescription(handler: unknown): string | undefined {\n return (handler as any)?.[MCP_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}\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: any) => {\n if (c.req.method === \"OPTIONS\") return new Response(null, { headers: CORS_HEADERS });\n\n const loader = c.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: any) => {\n if (c.req.method === \"OPTIONS\") return new Response(null, { headers: CORS_HEADERS });\n\n const server = createServer();\n for (const route of routes) registerTool(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(app: Hono<any>): Route[] {\n const routes: Route[] = [];\n const seen = new Set<string>();\n\n const honoRoutes = (app as any).routes as Array<{\n path: string;\n method: string;\n handler: Function;\n }>;\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 });\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 registerTool(server: McpServer, route: Route, app: Hono<any>): void {\n const toolName = generateToolName(route.method, route.path);\n const pathParams = extractPathParams(route.path);\n\n const inputShape: Record<string, z.ZodType> = {};\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 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 },\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 text = contentType.includes(\"application/json\")\n ? JSON.stringify(await response.json(), null, 2)\n : 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(\n loader: WorkerLoader,\n code: string,\n app: Hono<any>,\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(\n server: McpServer,\n routes: Route[],\n app: Hono<any>,\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"],"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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono-mcp-server",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "hono app as mcp server",
5
5
  "keywords": [
6
6
  "api",
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { mcp, describe } from "./mcp";
2
- export type { McpOptions } from "./mcp";
1
+ export { mcp, registerTool } from "./mcp";
2
+ export type { McpOptions, DescribeConfig } from "./mcp";
package/src/mcp.test.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { describe as testDescribe, it, expect } from "vitest";
2
2
  import { Hono } from "hono";
3
- import { mcp, describe } from "./mcp";
3
+ import { z } from "zod";
4
+ import { mcp, registerTool } from "./mcp";
4
5
 
5
6
  testDescribe("mcp", () => {
6
7
  it("adds /mcp endpoint to app", async () => {
@@ -64,10 +65,7 @@ testDescribe("mcp", () => {
64
65
  });
65
66
 
66
67
  it("uses custom descriptions from describe()", async () => {
67
- const app = new Hono().get(
68
- "/users",
69
- describe("My custom description", (c) => c.json([])),
70
- );
68
+ const app = new Hono().get("/users", registerTool("My custom description"), (c) => c.json([]));
71
69
 
72
70
  const wrapped = mcp(app, { name: "Test", version: "1.0.0" });
73
71
 
@@ -138,7 +136,6 @@ testDescribe("mcp", () => {
138
136
  );
139
137
 
140
138
  const text = await res.text();
141
- // JSON is escaped in the MCP response
142
139
  expect(text).toContain('\\"id\\": \\"42\\"');
143
140
  expect(text).toContain("Bob");
144
141
  });
@@ -493,12 +490,117 @@ testDescribe("mcp", () => {
493
490
  });
494
491
  });
495
492
 
496
- testDescribe("describe", () => {
497
- it("attaches description to handler", () => {
498
- const handler = (c: any) => c.json([]);
499
- const described = describe("Test description", handler);
493
+ testDescribe("registerTool", () => {
494
+ it("returns middleware with string description", () => {
495
+ const middleware = registerTool("Test description");
496
+ expect(typeof middleware).toBe("function");
497
+ });
498
+
499
+ it("returns middleware with config object", () => {
500
+ const middleware = registerTool({ description: "Test description" });
501
+ expect(typeof middleware).toBe("function");
502
+ });
503
+
504
+ it("uses inputSchema from describe config", async () => {
505
+ const app = new Hono().post(
506
+ "/users",
507
+ registerTool({
508
+ description: "Create a new user",
509
+ inputSchema: {
510
+ name: z.string().describe("User name"),
511
+ email: z.string().email().describe("User email"),
512
+ },
513
+ }),
514
+ async (c) => {
515
+ const { name } = c.req.valid("json");
516
+ return c.json({ id: 1, name });
517
+ },
518
+ );
519
+
520
+ const wrapped = mcp(app, { name: "Test", version: "1.0.0" });
521
+
522
+ const res = await wrapped.fetch(
523
+ new Request("http://localhost/mcp", {
524
+ method: "POST",
525
+ headers: {
526
+ "Content-Type": "application/json",
527
+ Accept: "application/json, text/event-stream",
528
+ },
529
+ body: JSON.stringify({
530
+ jsonrpc: "2.0",
531
+ id: 1,
532
+ method: "tools/list",
533
+ params: {},
534
+ }),
535
+ }),
536
+ );
537
+
538
+ const text = await res.text();
539
+ expect(text).toContain("Create a new user");
540
+ expect(text).toContain("name");
541
+ expect(text).toContain("email");
542
+ expect(text).toContain("User name");
543
+ expect(text).toContain("User email");
544
+ });
545
+
546
+ it("returns structuredContent when outputSchema is defined", async () => {
547
+ const app = new Hono().get(
548
+ "/users",
549
+ registerTool({
550
+ description: "List users",
551
+ outputSchema: {
552
+ users: z.array(z.object({ id: z.number(), name: z.string() })),
553
+ },
554
+ }),
555
+ (c) => c.json({ users: [{ id: 1, name: "Alice" }] }),
556
+ );
557
+
558
+ const wrapped = mcp(app, { name: "Test", version: "1.0.0" });
500
559
 
501
- expect(described).toBe(handler);
502
- // The description is stored on the handler
560
+ const res = await wrapped.fetch(
561
+ new Request("http://localhost/mcp", {
562
+ method: "POST",
563
+ headers: {
564
+ "Content-Type": "application/json",
565
+ Accept: "application/json, text/event-stream",
566
+ },
567
+ body: JSON.stringify({
568
+ jsonrpc: "2.0",
569
+ id: 1,
570
+ method: "tools/call",
571
+ params: { name: "get_users", arguments: {} },
572
+ }),
573
+ }),
574
+ );
575
+
576
+ const text = await res.text();
577
+ expect(text).toContain("structuredContent");
578
+ expect(text).toContain("Alice");
579
+ });
580
+
581
+ it("returns plain text for text responses", async () => {
582
+ const app = new Hono().get("/hello", registerTool("Say hello"), (c) => c.text("Hello, World!"));
583
+
584
+ const wrapped = mcp(app, { name: "Test", version: "1.0.0" });
585
+
586
+ const res = await wrapped.fetch(
587
+ new Request("http://localhost/mcp", {
588
+ method: "POST",
589
+ headers: {
590
+ "Content-Type": "application/json",
591
+ Accept: "application/json, text/event-stream",
592
+ },
593
+ body: JSON.stringify({
594
+ jsonrpc: "2.0",
595
+ id: 1,
596
+ method: "tools/call",
597
+ params: { name: "get_hello", arguments: {} },
598
+ }),
599
+ }),
600
+ );
601
+
602
+ const text = await res.text();
603
+ expect(text).toContain("Hello, World!");
604
+ expect(text).not.toContain("structuredContent");
503
605
  });
504
606
  });
package/src/mcp.ts CHANGED
@@ -1,26 +1,112 @@
1
1
  import { Hono } from "hono";
2
- import type { Env, Handler } from "hono";
2
+ import type { Context, Env, Handler, MiddlewareHandler, ValidationTargets } from "hono";
3
+ import type { RouterRoute, H } from "hono/types";
4
+ import { validator } from "hono/validator";
3
5
  import { z } from "zod";
4
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
5
8
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
6
9
 
7
- const MCP_DESCRIPTION = Symbol("mcp_description");
10
+ // Configuration for registerTool()
11
+ export interface DescribeConfig<
12
+ TInput extends z.ZodRawShape = z.ZodRawShape,
13
+ TOutput extends z.ZodRawShape = z.ZodRawShape,
14
+ > {
15
+ description: string;
16
+ inputSchema?: TInput;
17
+ outputSchema?: TOutput;
18
+ annotations?: ToolAnnotations;
19
+ }
20
+
21
+ // Internal metadata storage
22
+ interface ToolMetadata {
23
+ description: string;
24
+ inputSchema?: z.ZodRawShape;
25
+ outputSchema?: z.ZodRawShape;
26
+ annotations?: ToolAnnotations;
27
+ }
28
+
29
+ const toolMetadata = new WeakMap<Function, ToolMetadata>();
8
30
 
9
31
  /**
10
- * Add a description to a route handler for MCP tool generation.
32
+ * Register a route as an MCP tool with description and optional schemas.
33
+ * Always returns middleware - the actual handler should be a separate function.
11
34
  *
12
35
  * @example
13
36
  * ```ts
14
- * app.get('/users', describe('List all users', (c) => c.json([])))
37
+ * // Simple description
38
+ * app.get('/users', registerTool('List all users'), (c) => c.json([]))
39
+ *
40
+ * // With config
41
+ * app.get('/users', registerTool({ description: 'List all users' }), (c) => c.json([]))
42
+ *
43
+ * // With inputSchema - use c.req.valid('json') for typed input
44
+ * app.post('/users',
45
+ * registerTool({ description: 'Create a user', inputSchema: { name: z.string() } }),
46
+ * async (c) => {
47
+ * const { name } = c.req.valid('json') // typed!
48
+ * return c.json({ id: 1, name })
49
+ * }
50
+ * )
15
51
  * ```
16
52
  */
17
- export function describe<H extends Handler>(description: string, handler: H): H {
18
- (handler as any)[MCP_DESCRIPTION] = description;
19
- return handler;
53
+ // Overload: string description
54
+ export function registerTool(
55
+ description: string,
56
+ ): MiddlewareHandler;
57
+ // Overload: config with inputSchema - provides typed validation
58
+ export function registerTool<TInput extends z.ZodRawShape, TOutput extends z.ZodRawShape = z.ZodRawShape>(
59
+ config: DescribeConfig<TInput, TOutput> & { inputSchema: TInput },
60
+ ): MiddlewareHandler<Env, string, { in: { json: z.infer<z.ZodObject<TInput>> }; out: { json: z.infer<z.ZodObject<TInput>> } }>;
61
+ // Overload: config without inputSchema
62
+ export function registerTool<TOutput extends z.ZodRawShape>(
63
+ config: DescribeConfig<z.ZodRawShape, TOutput>,
64
+ ): MiddlewareHandler;
65
+ // Implementation
66
+ export function registerTool(
67
+ descriptionOrConfig: string | DescribeConfig,
68
+ ): MiddlewareHandler {
69
+ const metadata: ToolMetadata =
70
+ typeof descriptionOrConfig === "string"
71
+ ? { description: descriptionOrConfig }
72
+ : {
73
+ description: descriptionOrConfig.description,
74
+ inputSchema: descriptionOrConfig.inputSchema,
75
+ outputSchema: descriptionOrConfig.outputSchema,
76
+ annotations: descriptionOrConfig.annotations,
77
+ };
78
+
79
+ // If inputSchema is defined, return validating middleware
80
+ if (metadata.inputSchema) {
81
+ const schema = z.object(metadata.inputSchema);
82
+ const middleware = validator("json", (value) => {
83
+ const result = schema.safeParse(value);
84
+ if (!result.success) {
85
+ throw new Error(result.error.message);
86
+ }
87
+ return result.data;
88
+ });
89
+ toolMetadata.set(middleware, metadata);
90
+ return middleware;
91
+ }
92
+
93
+ // Otherwise return pass-through middleware that just stores metadata
94
+ const middleware: MiddlewareHandler = async (_c, next) => {
95
+ await next();
96
+ };
97
+ toolMetadata.set(middleware, metadata);
98
+ return middleware;
99
+ }
100
+
101
+ function getToolMetadata(handler: unknown): ToolMetadata | undefined {
102
+ if (typeof handler === "function") {
103
+ return toolMetadata.get(handler);
104
+ }
105
+ return undefined;
20
106
  }
21
107
 
22
108
  function getDescription(handler: unknown): string | undefined {
23
- return (handler as any)?.[MCP_DESCRIPTION];
109
+ return getToolMetadata(handler)?.description;
24
110
  }
25
111
 
26
112
  // WorkerLoader interface (matches @cloudflare/workers-types)
@@ -55,6 +141,7 @@ interface Route {
55
141
  method: HttpMethod;
56
142
  path: string;
57
143
  description: string;
144
+ handler: H;
58
145
  }
59
146
 
60
147
  const CORS_HEADERS = {
@@ -121,10 +208,11 @@ export function mcp<E extends Env>(app: Hono<E>, options: McpOptions): Hono<E> {
121
208
  );
122
209
 
123
210
  const handleMcp = codemode
124
- ? async (c: any) => {
211
+ ? async (c: Context<E>) => {
125
212
  if (c.req.method === "OPTIONS") return new Response(null, { headers: CORS_HEADERS });
126
213
 
127
- const loader = c.env?.[loaderBinding] as WorkerLoader | undefined;
214
+ const env = c.env as Record<string, unknown> | undefined;
215
+ const loader = env?.[loaderBinding] as WorkerLoader | undefined;
128
216
  if (!loader) {
129
217
  return new Response(
130
218
  `Codemode requires ${loaderBinding} binding. Add worker_loaders to wrangler.jsonc.`,
@@ -139,11 +227,11 @@ export function mcp<E extends Env>(app: Hono<E>, options: McpOptions): Hono<E> {
139
227
  server.connect(transport);
140
228
  return withCors(await transport.handleRequest(c.req.raw));
141
229
  }
142
- : async (c: any) => {
230
+ : async (c: Context<E>) => {
143
231
  if (c.req.method === "OPTIONS") return new Response(null, { headers: CORS_HEADERS });
144
232
 
145
233
  const server = createServer();
146
- for (const route of routes) registerTool(server, route, app);
234
+ for (const route of routes) registerRouteAsTool(server, route, app);
147
235
 
148
236
  const transport = new WebStandardStreamableHTTPServerTransport();
149
237
  server.connect(transport);
@@ -156,15 +244,11 @@ export function mcp<E extends Env>(app: Hono<E>, options: McpOptions): Hono<E> {
156
244
  return app;
157
245
  }
158
246
 
159
- function extractRoutes(app: Hono<any>): Route[] {
247
+ function extractRoutes<E extends Env>(app: Hono<E>): Route[] {
160
248
  const routes: Route[] = [];
161
249
  const seen = new Set<string>();
162
250
 
163
- const honoRoutes = (app as any).routes as Array<{
164
- path: string;
165
- method: string;
166
- handler: Function;
167
- }>;
251
+ const honoRoutes: RouterRoute[] = app.routes;
168
252
 
169
253
  if (!honoRoutes) return routes;
170
254
 
@@ -178,7 +262,7 @@ function extractRoutes(app: Hono<any>): Route[] {
178
262
  seen.add(key);
179
263
 
180
264
  const description = getDescription(route.handler) || generateDescription(method, route.path);
181
- routes.push({ method: method as HttpMethod, path: route.path, description });
265
+ routes.push({ method: method as HttpMethod, path: route.path, description, handler: route.handler });
182
266
  }
183
267
 
184
268
  return routes;
@@ -215,19 +299,35 @@ function extractPathParams(path: string): string[] {
215
299
  return matches ? matches.map((m) => m.slice(1)) : [];
216
300
  }
217
301
 
218
- function registerTool(server: McpServer, route: Route, app: Hono<any>): void {
302
+ function registerRouteAsTool<E extends Env>(server: McpServer, route: Route, app: Hono<E>): void {
219
303
  const toolName = generateToolName(route.method, route.path);
220
304
  const pathParams = extractPathParams(route.path);
221
-
222
- const inputShape: Record<string, z.ZodType> = {};
223
- for (const param of pathParams) {
224
- inputShape[param] = z.string().describe(`Path parameter: ${param}`);
225
- }
226
- if (["POST", "PUT", "PATCH"].includes(route.method)) {
227
- inputShape["body"] = z.record(z.unknown()).optional().describe("Request body as JSON object");
228
- }
229
- if (["GET", "DELETE"].includes(route.method)) {
230
- inputShape["query"] = z.record(z.string()).optional().describe("Query parameters");
305
+ const metadata = getToolMetadata(route.handler);
306
+
307
+ // Use metadata.inputSchema if available, otherwise generate default schema
308
+ let inputShape: Record<string, z.ZodType>;
309
+
310
+ if (metadata?.inputSchema) {
311
+ // Use the provided input schema from metadata
312
+ inputShape = { ...metadata.inputSchema };
313
+ // Still need to add path params if not already present
314
+ for (const param of pathParams) {
315
+ if (!(param in inputShape)) {
316
+ inputShape[param] = z.string().describe(`Path parameter: ${param}`);
317
+ }
318
+ }
319
+ } else {
320
+ // Generate default schema
321
+ inputShape = {};
322
+ for (const param of pathParams) {
323
+ inputShape[param] = z.string().describe(`Path parameter: ${param}`);
324
+ }
325
+ if (["POST", "PUT", "PATCH"].includes(route.method)) {
326
+ inputShape["body"] = z.record(z.unknown()).optional().describe("Request body as JSON object");
327
+ }
328
+ if (["GET", "DELETE"].includes(route.method)) {
329
+ inputShape["query"] = z.record(z.string()).optional().describe("Query parameters");
330
+ }
231
331
  }
232
332
 
233
333
  server.registerTool(
@@ -235,6 +335,8 @@ function registerTool(server: McpServer, route: Route, app: Hono<any>): void {
235
335
  {
236
336
  description: `${route.description}\n\n${route.method} ${route.path}`,
237
337
  inputSchema: Object.keys(inputShape).length > 0 ? inputShape : undefined,
338
+ ...(metadata?.outputSchema && { outputSchema: metadata.outputSchema }),
339
+ ...(metadata?.annotations && { annotations: metadata.annotations }),
238
340
  },
239
341
  async (params: Record<string, unknown>) => {
240
342
  let url = route.path;
@@ -262,9 +364,26 @@ function registerTool(server: McpServer, route: Route, app: Hono<any>): void {
262
364
  try {
263
365
  const response = await app.fetch(new Request(`http://internal${url}`, init));
264
366
  const contentType = response.headers.get("content-type") || "";
265
- const text = contentType.includes("application/json")
266
- ? JSON.stringify(await response.json(), null, 2)
267
- : await response.text();
367
+ const isJson = contentType.includes("application/json");
368
+
369
+ if (isJson) {
370
+ const json = await response.json();
371
+ // Return structured content if outputSchema is defined
372
+ if (metadata?.outputSchema) {
373
+ return {
374
+ structuredContent: json,
375
+ content: [{ type: "text" as const, text: JSON.stringify(json, null, 2) }],
376
+ isError: !response.ok,
377
+ };
378
+ }
379
+ return {
380
+ content: [{ type: "text" as const, text: JSON.stringify(json, null, 2) }],
381
+ isError: !response.ok,
382
+ };
383
+ }
384
+
385
+ // Plain text response
386
+ const text = await response.text();
268
387
  return { content: [{ type: "text" as const, text }], isError: !response.ok };
269
388
  } catch (error) {
270
389
  return {
@@ -336,10 +455,10 @@ async function executeSearch(
336
455
  return (await entrypoint.evaluate()) as { result?: unknown; error?: string };
337
456
  }
338
457
 
339
- async function executeCode(
458
+ async function executeCode<E extends Env>(
340
459
  loader: WorkerLoader,
341
460
  code: string,
342
- app: Hono<any>,
461
+ app: Hono<E>,
343
462
  ): Promise<{ result?: unknown; error?: string }> {
344
463
  const workerId = `execute-${crypto.randomUUID()}`;
345
464
 
@@ -375,10 +494,10 @@ async function executeCode(
375
494
  return (await entrypoint.evaluate(fetch)) as { result?: unknown; error?: string };
376
495
  }
377
496
 
378
- function registerCodemodeTools(
497
+ function registerCodemodeTools<E extends Env>(
379
498
  server: McpServer,
380
499
  routes: Route[],
381
- app: Hono<any>,
500
+ app: Hono<E>,
382
501
  loader: WorkerLoader,
383
502
  ): void {
384
503
  const apiSchema = routesToApiSchema(routes);