hono-mcp-server 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # MCP Server for Hono
2
+
3
+ Expose your [Hono](https://hono.dev) API endpoints as [MCP](https://modelcontextprotocol.io) tools.
4
+
5
+ ## Usage
6
+
7
+ ```ts
8
+ import { Hono } from "hono";
9
+ import { mcp, describe } from "hono-mcp-server";
10
+
11
+ 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
+ )
20
+ .post(
21
+ "/users",
22
+ describe("Create a user", async (c) => c.json(await c.req.json(), 201)),
23
+ );
24
+
25
+ export default mcp(app, {
26
+ name: "Users API",
27
+ version: "1.0.0",
28
+ });
29
+ ```
30
+
31
+ This adds an `/mcp` endpoint that exposes your routes as MCP tools.
32
+
33
+ ## Options
34
+
35
+ ```ts
36
+ mcp(app, {
37
+ name: "API Name", // required
38
+ version: "1.0.0", // required
39
+ description: "...", // optional
40
+ instructions: "...", // optional
41
+ mcpPath: "/mcp", // optional, default: "/mcp"
42
+ codemode: false, // optional, see below
43
+ });
44
+ ```
45
+
46
+ ## Codemode
47
+
48
+ Instead of exposing individual routes as tools, codemode exposes `search` and `execute` tools for dynamic API interaction. Requires [Cloudflare Worker Loader](https://developers.cloudflare.com/workers/runtime-apis/bindings/worker-loader/).
49
+
50
+ ```ts
51
+ export default mcp(app, {
52
+ name: "API",
53
+ version: "1.0.0",
54
+ codemode: true,
55
+ });
56
+ ```
57
+
58
+ ```jsonc
59
+ // wrangler.jsonc
60
+ {
61
+ "worker_loaders": [{ "binding": "LOADER" }],
62
+ }
63
+ ```
@@ -0,0 +1,48 @@
1
+ import { Env, Handler, Hono } from "hono";
2
+
3
+ //#region src/mcp.d.ts
4
+ /**
5
+ * Add a description to a route handler for MCP tool generation.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * app.get('/users', describe('List all users', (c) => c.json([])))
10
+ * ```
11
+ */
12
+ declare function describe<H extends Handler>(description: string, handler: H): H;
13
+ interface McpOptions {
14
+ name: string;
15
+ version: string;
16
+ title?: string;
17
+ description?: string;
18
+ instructions?: string;
19
+ mcpPath?: string;
20
+ /** Enable codemode - exposes search/execute tools instead of per-route tools. Requires LOADER binding. */
21
+ codemode?: boolean;
22
+ /** Binding name for Worker Loader (default: "LOADER"). Only used when codemode is true. */
23
+ loaderBinding?: string;
24
+ }
25
+ /**
26
+ * Wraps a Hono app to add an MCP endpoint that exposes routes as tools.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * import { mcp, describe } from 'hono-mcp'
31
+ *
32
+ * const app = new Hono()
33
+ * .get('/users', describe('List all users', (c) => c.json([])))
34
+ * .get('/users/:id', describe('Get user by ID', (c) => c.json({ id: c.req.param('id') })));
35
+ *
36
+ * export default mcp(app, { name: 'Users API', version: '1.0.0' });
37
+ * ```
38
+ *
39
+ * @example Codemode (requires Worker Loader binding)
40
+ * ```ts
41
+ * export default mcp(app, { name: 'API', version: '1.0.0', codemode: true });
42
+ * ```
43
+ *
44
+ */
45
+ declare function mcp<E extends Env>(app: Hono<E>, options: McpOptions): Hono<E>;
46
+ //#endregion
47
+ export { type McpOptions, describe, mcp };
48
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +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"}
package/dist/index.mjs ADDED
@@ -0,0 +1,310 @@
1
+ import { z } from "zod";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
4
+
5
+ //#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;
18
+ }
19
+ function getDescription(handler) {
20
+ return handler?.[MCP_DESCRIPTION];
21
+ }
22
+ const CORS_HEADERS = {
23
+ "Access-Control-Allow-Origin": "*",
24
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
25
+ "Access-Control-Allow-Headers": "Content-Type, Accept, mcp-session-id, mcp-protocol-version",
26
+ "Access-Control-Expose-Headers": "mcp-session-id",
27
+ "Access-Control-Max-Age": "86400"
28
+ };
29
+ function withCors(response) {
30
+ const newHeaders = new Headers(response.headers);
31
+ for (const [key, value] of Object.entries(CORS_HEADERS)) newHeaders.set(key, value);
32
+ return new Response(response.body, {
33
+ status: response.status,
34
+ statusText: response.statusText,
35
+ headers: newHeaders
36
+ });
37
+ }
38
+ /**
39
+ * Wraps a Hono app to add an MCP endpoint that exposes routes as tools.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * import { mcp, describe } from 'hono-mcp'
44
+ *
45
+ * const app = new Hono()
46
+ * .get('/users', describe('List all users', (c) => c.json([])))
47
+ * .get('/users/:id', describe('Get user by ID', (c) => c.json({ id: c.req.param('id') })));
48
+ *
49
+ * export default mcp(app, { name: 'Users API', version: '1.0.0' });
50
+ * ```
51
+ *
52
+ * @example Codemode (requires Worker Loader binding)
53
+ * ```ts
54
+ * export default mcp(app, { name: 'API', version: '1.0.0', codemode: true });
55
+ * ```
56
+ *
57
+ */
58
+ function mcp(app, options) {
59
+ const { mcpPath = "/mcp", codemode = false, loaderBinding = "LOADER" } = options;
60
+ const routes = extractRoutes(app);
61
+ const serverInfo = {
62
+ name: options.name,
63
+ version: options.version,
64
+ ...options.title && { title: options.title },
65
+ ...options.description && { description: options.description }
66
+ };
67
+ 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);
68
+ const handleMcp = codemode ? async (c) => {
69
+ if (c.req.method === "OPTIONS") return new Response(null, { headers: CORS_HEADERS });
70
+ const loader = c.env?.[loaderBinding];
71
+ if (!loader) return new Response(`Codemode requires ${loaderBinding} binding. Add worker_loaders to wrangler.jsonc.`, { status: 500 });
72
+ const server = createServer();
73
+ registerCodemodeTools(server, routes, app, loader);
74
+ const transport = new WebStandardStreamableHTTPServerTransport();
75
+ server.connect(transport);
76
+ return withCors(await transport.handleRequest(c.req.raw));
77
+ } : async (c) => {
78
+ if (c.req.method === "OPTIONS") return new Response(null, { headers: CORS_HEADERS });
79
+ const server = createServer();
80
+ for (const route of routes) registerTool(server, route, app);
81
+ const transport = new WebStandardStreamableHTTPServerTransport();
82
+ server.connect(transport);
83
+ return withCors(await transport.handleRequest(c.req.raw));
84
+ };
85
+ app.all(`${mcpPath}/*`, handleMcp);
86
+ app.all(mcpPath, handleMcp);
87
+ return app;
88
+ }
89
+ function extractRoutes(app) {
90
+ const routes = [];
91
+ const seen = /* @__PURE__ */ new Set();
92
+ const honoRoutes = app.routes;
93
+ if (!honoRoutes) return routes;
94
+ for (const route of honoRoutes) {
95
+ const method = route.method.toUpperCase();
96
+ if (![
97
+ "GET",
98
+ "POST",
99
+ "PUT",
100
+ "PATCH",
101
+ "DELETE"
102
+ ].includes(method)) continue;
103
+ if (route.path.startsWith("/mcp")) continue;
104
+ const key = `${method} ${route.path}`;
105
+ if (seen.has(key)) continue;
106
+ seen.add(key);
107
+ const description = getDescription(route.handler) || generateDescription(method, route.path);
108
+ routes.push({
109
+ method,
110
+ path: route.path,
111
+ description
112
+ });
113
+ }
114
+ return routes;
115
+ }
116
+ function generateDescription(method, path) {
117
+ const resource = path.split("/").filter((p) => p && !p.startsWith(":")).pop() || "resource";
118
+ return `${{
119
+ GET: path.includes(":") ? "Get" : "List",
120
+ POST: "Create",
121
+ PUT: "Update",
122
+ PATCH: "Update",
123
+ DELETE: "Delete"
124
+ }[method] || method} ${resource}`;
125
+ }
126
+ function generateToolName(method, path) {
127
+ const cleanPath = path.replace(/^\//, "").replace(/\/:([^/]+)/g, "_by_$1").replace(/\//g, "_").replace(/[^a-zA-Z0-9_]/g, "");
128
+ return `${method.toLowerCase()}_${cleanPath || "root"}`;
129
+ }
130
+ function extractPathParams(path) {
131
+ const matches = path.match(/:([^/]+)/g);
132
+ return matches ? matches.map((m) => m.slice(1)) : [];
133
+ }
134
+ function registerTool(server, route, app) {
135
+ const toolName = generateToolName(route.method, route.path);
136
+ 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");
145
+ server.registerTool(toolName, {
146
+ description: `${route.description}\n\n${route.method} ${route.path}`,
147
+ inputSchema: Object.keys(inputShape).length > 0 ? inputShape : void 0
148
+ }, async (params) => {
149
+ let url = route.path;
150
+ for (const param of pathParams) {
151
+ const value = params[param];
152
+ if (value !== void 0) url = url.replace(`:${param}`, encodeURIComponent(String(value)));
153
+ }
154
+ const query = params["query"];
155
+ if (query && Object.keys(query).length > 0) url = `${url}?${new URLSearchParams(query).toString()}`;
156
+ const body = params["body"];
157
+ const init = {
158
+ method: route.method,
159
+ headers: {
160
+ "Content-Type": "application/json",
161
+ Accept: "application/json"
162
+ }
163
+ };
164
+ if (body && [
165
+ "POST",
166
+ "PUT",
167
+ "PATCH"
168
+ ].includes(route.method)) init.body = JSON.stringify(body);
169
+ try {
170
+ const response = await app.fetch(new Request(`http://internal${url}`, init));
171
+ return {
172
+ content: [{
173
+ type: "text",
174
+ text: (response.headers.get("content-type") || "").includes("application/json") ? JSON.stringify(await response.json(), null, 2) : await response.text()
175
+ }],
176
+ isError: !response.ok
177
+ };
178
+ } catch (error) {
179
+ return {
180
+ content: [{
181
+ type: "text",
182
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
183
+ }],
184
+ isError: true
185
+ };
186
+ }
187
+ });
188
+ }
189
+ function routesToApiSchema(routes) {
190
+ return routes.map((route) => {
191
+ const endpoint = {
192
+ method: route.method,
193
+ path: route.path,
194
+ description: route.description
195
+ };
196
+ if ([
197
+ "POST",
198
+ "PUT",
199
+ "PATCH"
200
+ ].includes(route.method)) endpoint.body = "JSON object";
201
+ return endpoint;
202
+ });
203
+ }
204
+ async function executeSearch(loader, code, apiSchema) {
205
+ const workerId = `search-${crypto.randomUUID()}`;
206
+ return await loader.get(workerId, () => ({
207
+ compatibilityDate: "2026-01-14",
208
+ compatibilityFlags: ["nodejs_compat"],
209
+ mainModule: "worker.js",
210
+ modules: { "worker.js": `
211
+ import { WorkerEntrypoint } from "cloudflare:workers";
212
+ const endpoints = ${JSON.stringify(apiSchema)};
213
+ export default class SearchExecutor extends WorkerEntrypoint {
214
+ async evaluate() {
215
+ try {
216
+ const result = await (${code})();
217
+ return { result };
218
+ } catch (err) {
219
+ return { error: err.message };
220
+ }
221
+ }
222
+ }
223
+ ` }
224
+ })).getEntrypoint().evaluate();
225
+ }
226
+ async function executeCode(loader, code, app) {
227
+ const workerId = `execute-${crypto.randomUUID()}`;
228
+ const worker = loader.get(workerId, () => ({
229
+ compatibilityDate: "2026-01-14",
230
+ compatibilityFlags: ["nodejs_compat"],
231
+ mainModule: "worker.js",
232
+ modules: { "worker.js": `
233
+ import { WorkerEntrypoint } from "cloudflare:workers";
234
+
235
+ export default class ExecuteWorker extends WorkerEntrypoint {
236
+ async evaluate(fetch) {
237
+ try {
238
+ const result = await (${code})();
239
+ return { result };
240
+ } catch (err) {
241
+ return { error: err.message };
242
+ }
243
+ }
244
+ }
245
+ ` }
246
+ }));
247
+ const fetch = async (path, options = {}) => {
248
+ const response = await app.fetch(new Request(`http://internal${path}`, options));
249
+ return (response.headers.get("content-type") || "").includes("application/json") ? response.json() : response.text();
250
+ };
251
+ return await worker.getEntrypoint().evaluate(fetch);
252
+ }
253
+ function registerCodemodeTools(server, routes, app, loader) {
254
+ const apiSchema = routesToApiSchema(routes);
255
+ server.registerTool("search", {
256
+ description: `Search available API endpoints.
257
+
258
+ Available in your code:
259
+ const endpoints = [...] // Array of { method, path, description, body? }
260
+
261
+ Example:
262
+ async () => endpoints.filter(e => e.path.includes('users'))`,
263
+ inputSchema: { code: z.string().describe("JavaScript async arrow function to search endpoints") }
264
+ }, async ({ code }) => {
265
+ const result = await executeSearch(loader, code, apiSchema);
266
+ if (result.error) return {
267
+ content: [{
268
+ type: "text",
269
+ text: `Error: ${result.error}`
270
+ }],
271
+ isError: true
272
+ };
273
+ return { content: [{
274
+ type: "text",
275
+ text: JSON.stringify(result.result, null, 2)
276
+ }] };
277
+ });
278
+ server.registerTool("execute", {
279
+ description: `Execute code against the API.
280
+
281
+ Types:
282
+ interface RequestInit {
283
+ method?: string;
284
+ headers?: Record<string, string>;
285
+ body?: string;
286
+ }
287
+ declare function fetch(path: string, options?: RequestInit): Promise<unknown>;
288
+
289
+ Example:
290
+ async () => await fetch('/users')`,
291
+ inputSchema: { code: z.string().describe("JavaScript async arrow function to execute") }
292
+ }, async ({ code }) => {
293
+ const result = await executeCode(loader, code, app);
294
+ if (result.error) return {
295
+ content: [{
296
+ type: "text",
297
+ text: `Error: ${result.error}`
298
+ }],
299
+ isError: true
300
+ };
301
+ return { content: [{
302
+ type: "text",
303
+ text: JSON.stringify(result.result, null, 2)
304
+ }] };
305
+ });
306
+ }
307
+
308
+ //#endregion
309
+ export { describe, mcp };
310
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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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"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "hono-mcp-server",
3
+ "version": "0.0.1",
4
+ "description": "hono app as mcp server",
5
+ "keywords": [
6
+ "api",
7
+ "cloudflare-workers",
8
+ "hono",
9
+ "mcp",
10
+ "model-context-protocol",
11
+ "server"
12
+ ],
13
+ "license": "MIT",
14
+ "author": "Matt Carey <mcarey@cloudflare.com>",
15
+ "workspaces": [
16
+ "examples/*"
17
+ ],
18
+ "files": [
19
+ "dist",
20
+ "src"
21
+ ],
22
+ "type": "module",
23
+ "main": "dist/index.mjs",
24
+ "types": "dist/index.d.mts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.mts",
28
+ "import": "./dist/index.mjs"
29
+ }
30
+ },
31
+ "scripts": {
32
+ "build": "tsdown",
33
+ "prepublishOnly": "npm run build",
34
+ "check": "npm run typecheck && npm run format:check && npm run lint",
35
+ "typecheck": "tsc --noEmit",
36
+ "format": "oxfmt --write .",
37
+ "format:check": "oxfmt .",
38
+ "lint": "oxlint --ignore-pattern node_modules --ignore-pattern .wrangler --ignore-pattern dist",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "examples:types": "npm run types --workspaces"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.25.2",
45
+ "hono": "^4.0.0",
46
+ "zod": "^3.25.0"
47
+ },
48
+ "devDependencies": {
49
+ "oxfmt": "^0.24.0",
50
+ "oxlint": "^1.39.0",
51
+ "tsdown": "^0.20.0-beta.3",
52
+ "typescript": "^5.7.3",
53
+ "vitest": "^2.1.0",
54
+ "wrangler": "^4.59.2"
55
+ },
56
+ "peerDependencies": {
57
+ "hono": "^4.0.0"
58
+ }
59
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { mcp, describe } from "./mcp";
2
+ export type { McpOptions } from "./mcp";