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 +44 -10
- package/dist/index.d.mts +38 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +70 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +2 -2
- package/src/mcp.test.ts +114 -12
- package/src/mcp.ts +156 -37
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 {
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { mcp, registerTool } from "hono-mcp-server";
|
|
10
11
|
|
|
11
12
|
const app = new Hono()
|
|
12
|
-
.get(
|
|
13
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
80
|
+
export { type DescribeConfig, type McpOptions, mcp, registerTool };
|
|
48
81
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/mcp.ts"],"mappings":"
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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?.
|
|
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)
|
|
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
|
|
150
|
+
function registerRouteAsTool(server, route, app) {
|
|
135
151
|
const toolName = generateToolName(route.method, route.path);
|
|
136
152
|
const pathParams = extractPathParams(route.path);
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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:
|
|
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 {
|
|
352
|
+
export { mcp, registerTool };
|
|
310
353
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -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
package/src/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { 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 {
|
|
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("
|
|
497
|
-
it("
|
|
498
|
-
const
|
|
499
|
-
|
|
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
|
-
|
|
502
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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:
|
|
211
|
+
? async (c: Context<E>) => {
|
|
125
212
|
if (c.req.method === "OPTIONS") return new Response(null, { headers: CORS_HEADERS });
|
|
126
213
|
|
|
127
|
-
const
|
|
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:
|
|
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)
|
|
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<
|
|
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 =
|
|
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
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
266
|
-
|
|
267
|
-
|
|
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<
|
|
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<
|
|
500
|
+
app: Hono<E>,
|
|
382
501
|
loader: WorkerLoader,
|
|
383
502
|
): void {
|
|
384
503
|
const apiSchema = routesToApiSchema(routes);
|