mcp-expose 1.0.0

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/LICENSE +21 -0
  3. package/README.md +708 -0
  4. package/dist/adonisjs/index.cjs +708 -0
  5. package/dist/adonisjs/index.cjs.map +1 -0
  6. package/dist/adonisjs/index.d.cts +68 -0
  7. package/dist/adonisjs/index.d.ts +68 -0
  8. package/dist/adonisjs/index.js +684 -0
  9. package/dist/adonisjs/index.js.map +1 -0
  10. package/dist/express/index.cjs +789 -0
  11. package/dist/express/index.cjs.map +1 -0
  12. package/dist/express/index.d.cts +61 -0
  13. package/dist/express/index.d.ts +61 -0
  14. package/dist/express/index.js +763 -0
  15. package/dist/express/index.js.map +1 -0
  16. package/dist/fastify/index.cjs +660 -0
  17. package/dist/fastify/index.cjs.map +1 -0
  18. package/dist/fastify/index.d.cts +42 -0
  19. package/dist/fastify/index.d.ts +42 -0
  20. package/dist/fastify/index.js +635 -0
  21. package/dist/fastify/index.js.map +1 -0
  22. package/dist/hono/index.cjs +656 -0
  23. package/dist/hono/index.cjs.map +1 -0
  24. package/dist/hono/index.d.cts +44 -0
  25. package/dist/hono/index.d.ts +44 -0
  26. package/dist/hono/index.js +630 -0
  27. package/dist/hono/index.js.map +1 -0
  28. package/dist/index.cjs +720 -0
  29. package/dist/index.cjs.map +1 -0
  30. package/dist/index.d.cts +81 -0
  31. package/dist/index.d.ts +81 -0
  32. package/dist/index.js +686 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/koa/index.cjs +739 -0
  35. package/dist/koa/index.cjs.map +1 -0
  36. package/dist/koa/index.d.cts +68 -0
  37. package/dist/koa/index.d.ts +68 -0
  38. package/dist/koa/index.js +709 -0
  39. package/dist/koa/index.js.map +1 -0
  40. package/dist/nestjs/index.cjs +1117 -0
  41. package/dist/nestjs/index.cjs.map +1 -0
  42. package/dist/nestjs/index.d.cts +104 -0
  43. package/dist/nestjs/index.d.ts +104 -0
  44. package/dist/nestjs/index.js +1079 -0
  45. package/dist/nestjs/index.js.map +1 -0
  46. package/dist/server-fzkb-wJo.d.cts +194 -0
  47. package/dist/server-fzkb-wJo.d.ts +194 -0
  48. package/package.json +225 -0
package/README.md ADDED
@@ -0,0 +1,708 @@
1
+ # mcp-expose
2
+
3
+ [![CI](https://github.com/NITINKACHHADIYA/Node-MCP/actions/workflows/ci.yml/badge.svg)](https://github.com/NITINKACHHADIYA/Node-MCP/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/mcp-expose.svg)](https://www.npmjs.com/package/mcp-expose)
5
+ [![types](https://img.shields.io/npm/types/mcp-expose.svg)](https://www.npmjs.com/package/mcp-expose)
6
+ [![node](https://img.shields.io/node/v/mcp-expose.svg)](package.json)
7
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+
9
+ **Make your existing Node.js API agent-ready in minutes.**
10
+ `mcp-expose` turns selected HTTP routes of your **NestJS, Express, Fastify, Koa, Hono or AdonisJS** app into
11
+ [Model Context Protocol (MCP)](https://modelcontextprotocol.io) tools, so AI agents such as Claude, Cursor,
12
+ VS Code Copilot and ChatGPT can call them. Your existing **auth guards, DTO validation, rate limits and
13
+ logging keep working** because every tool call runs through your app's normal request pipeline.
14
+
15
+ ```ts
16
+ // NestJS
17
+ @Get(':id')
18
+ @McpTool({ description: 'Get an order by id' })
19
+ findOne(@Param('id') id: string) { ... }
20
+
21
+ // Express / Koa / Hono
22
+ app.get('/orders/:id', mcpTool({ description: 'Get an order by id' }), auth, getOrder);
23
+
24
+ // Fastify
25
+ app.get('/orders/:id', { schema, config: { mcp: { description: 'Get an order by id' } } }, getOrder);
26
+
27
+ // AdonisJS
28
+ router.get('/orders/:id', [OrdersController, 'show']).use(middleware.auth()).mcp({ description: 'Get an order by id' });
29
+ ```
30
+
31
+ - One decorator or marker per route, then add the module/plugin. No wrapper code to write.
32
+ - Zero runtime dependencies. Dual ESM/CJS. Node.js 20, 22 and 24, plus Bun, Deno and Workers for Hono.
33
+ - Supports the current **and the two previous major versions** of every framework, verified end to end.
34
+ - Speaks MCP Streamable HTTP (protocol `2024-11-05` → `2025-11-25`), stateless, so it scales horizontally and runs serverless.
35
+
36
+ ---
37
+
38
+ ## Table of contents
39
+
40
+ 1. [Why this library?](#why-this-library)
41
+ 2. [How it works](#how-it-works)
42
+ 3. [Supported frameworks](#supported-frameworks)
43
+ 4. [Installation](#installation)
44
+ 5. [Framework guides](#framework-guides)
45
+ - [NestJS](#nestjs) · [Express](#express) · [Fastify](#fastify) · [Koa](#koa) · [Hono](#hono) · [AdonisJS](#adonisjs) · [Any API via OpenAPI](#any-api-via-openapi-standalone-gateway)
46
+ 6. [Connect an AI client](#connect-an-ai-client)
47
+ 7. [Defining tool inputs (schemas)](#defining-tool-inputs-schemas)
48
+ 8. [Configuration reference](#configuration-reference)
49
+ 9. [Custom (non-HTTP) tools](#custom-non-http-tools)
50
+ 10. [Security checklist](#security-checklist)
51
+ 11. [Writing tools agents use well](#writing-tools-agents-use-well)
52
+ 12. [Limitations and roadmap](#limitations-and-roadmap)
53
+ 13. [Development](#development)
54
+ 14. [Versioning and support](#versioning-and-support)
55
+
56
+ ---
57
+
58
+ ## Why this library?
59
+
60
+ Many companies now want their APIs to be "agent-ready". The usual approach is a separate, hand-written MCP
61
+ server that re-implements each endpoint as a tool:
62
+
63
+ | Hand-written MCP wrapper | `mcp-expose` |
64
+ | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
65
+ | Duplicates every endpoint's input schema, auth and error handling | Reuses the route that already exists. The route is the tool. |
66
+ | Needs its own auth. The easy path is often one over-privileged API key | Forwards the caller's `Authorization`/cookies, so **your guards decide** per user |
67
+ | Validation logic drifts from the real API | Your `ValidationPipe`, Fastify schema, zod or Joi runs on every call |
68
+ | Rate limits and audit logs are bypassed or re-implemented | Every tool call is a real request through your middleware |
69
+ | A second service to deploy, version and monitor | Mounted at `/mcp` inside the app you already run |
70
+ | Tied to one framework | Same concepts for NestJS, Express, Fastify, Koa and Hono |
71
+
72
+ **Benefits for developers**
73
+
74
+ - **Minutes, not days.** Add `@McpTool()` to the endpoints you want to expose and import one module.
75
+ - **Single source of truth.** Schemas come from your DTOs, Fastify JSON schemas, zod or OpenAPI. Change the endpoint and the tool changes with it.
76
+ - **Secure by default.** Routes are opt-in. The app's own auth applies to each tool call. `Origin` checks protect against DNS rebinding.
77
+ - **Agent-friendly errors.** Your API's 400/401/404 responses are returned to the model as tool errors, so it can correct itself (for example "quantity must not be greater than 10").
78
+ - **Framework-agnostic core.** Moving from Express to Fastify, or to a NestJS monolith, keeps the same tool model.
79
+
80
+ ## How it works
81
+
82
+ ```
83
+ AI client (Claude, Cursor, …)
84
+ │ POST /mcp {"method":"tools/call","params":{"name":"create_order","arguments":{…}}}
85
+ │ Authorization: Bearer <user token>
86
+ ▼
87
+ ┌──────────────────────── your app ────────────────────────┐
88
+ │ /mcp endpoint (mcp-expose) │
89
+ │ 1. finds the route behind "create_order" │
90
+ │ 2. maps arguments → path params / query / JSON body │
91
+ │ 3. forwards Authorization, Cookie, X-Api-Key, IP │
92
+ │ 4. dispatches: POST /orders ────────────┐ │
93
+ │ ▼ │
94
+ │ middleware → auth guard → rate limit → validation → handler
95
+ │ │ │
96
+ │ 5. HTTP response → MCP tool result ◄──────┘ │
97
+ │ (2xx → content + structuredContent, 4xx/5xx → isError)
98
+ └───────────────────────────────────────────────────────────┘
99
+ ```
100
+
101
+ Dispatch uses the fastest option each framework supports safely:
102
+
103
+ - **Fastify** uses `fastify.inject()`, in process, with every hook, schema and plugin applied.
104
+ - **Hono** uses `app.request()`, in process, and runs on any runtime.
105
+ - **Express, Koa, NestJS and AdonisJS** use a loopback HTTP request to the port the MCP call arrived on, so the full stack runs, including anything outside the framework such as a reverse proxy module.
106
+
107
+ Tools are discovered **lazily on the first MCP request**, so the order you register routes and the MCP endpoint rarely matters (Fastify is the exception, see its guide).
108
+
109
+ ## Supported frameworks
110
+
111
+ | Framework | Import | How you mark a route | Schema source | Dispatch |
112
+ | --------------------------------- | --------------------- | ------------------------------ | ---------------------------------------------------- | --------------- |
113
+ | NestJS (Express or Fastify) | `mcp-expose/nestjs` | `@McpTool()` decorator | class-validator DTOs (+ `@ApiProperty` descriptions) | loopback |
114
+ | Express | `mcp-expose/express` | `mcpTool()` middleware | options (JSON Schema / zod) | loopback |
115
+ | Fastify | `mcp-expose/fastify` | `config: { mcp }` on the route | the route's own `schema` | `inject()` |
116
+ | Koa + @koa/router (or koa-router) | `mcp-expose/koa` | `mcpTool()` middleware | options | loopback |
117
+ | Hono | `mcp-expose/hono` | `mcpTool()` middleware | options | `app.request()` |
118
+ | AdonisJS | `mcp-expose/adonisjs` | `.mcp()` on the route | options, or a VineJS validator | loopback |
119
+ | Any HTTP API (any language) | `mcp-expose` | OpenAPI `x-mcp: true` | OpenAPI document | `fetch` |
120
+
121
+ ### Version compatibility
122
+
123
+ mcp-expose supports the **current major version of each framework and the two before it**. Every row
124
+ below runs in CI as a real project (see [`e2e/`](e2e/README.md)): the packed library is installed from its
125
+ tarball and the app is driven by the official MCP SDK client, on Node.js 20, 22 and 24.
126
+
127
+ | Framework | Supported majors | Notes |
128
+ | --------- | ---------------- | ------------------------------------------------------------------------------------ |
129
+ | NestJS | **12**, 11, 10 | Express and Fastify platforms; CommonJS and ESM projects; classic and v11 `tsconfig` |
130
+ | Express | **5**, 4, 3 | Express 3 has been unmaintained since 2015 (upgrade recommended) |
131
+ | Fastify | **5**, 4, 3 | Fastify 3 is end-of-life upstream |
132
+ | Koa | **3**, 2, 1 | Koa 1 (generator middleware) uses `mcpToolLegacy()` / `koaMcpLegacy()` |
133
+ | Hono | **4**, 3, 2 | Hono 2 and 3 are end-of-life upstream |
134
+ | AdonisJS | **7**, 6 | AdonisJS 7 needs Node.js 24. AdonisJS 5 is not supported (see below) |
135
+
136
+ **AdonisJS 5** (last release November 2022) is the only exception. It uses a different architecture:
137
+ IoC-container imports such as `@ioc:Adonis/Core/Route`, and CommonJS builds. Supporting it would need a
138
+ separate adapter. Please [open an issue](https://github.com/NITINKACHHADIYA/Node-MCP/issues) if you need it.
139
+
140
+ ## Installation
141
+
142
+ ```bash
143
+ npm install mcp-expose
144
+ # or: pnpm add mcp-expose / yarn add mcp-expose / bun add mcp-expose
145
+ ```
146
+
147
+ Framework packages are optional peer dependencies. Use the ones you already have.
148
+ For NestJS DTO → schema generation, `class-validator` should be installed. Most Nest apps already have it.
149
+
150
+ ---
151
+
152
+ ## Framework guides
153
+
154
+ Each guide follows the same three steps: **1) install, 2) mark routes, 3) mount the endpoint**. Then
155
+ [connect a client](#connect-an-ai-client).
156
+
157
+ ### NestJS
158
+
159
+ **Step 1: import the module** (once, in your root module):
160
+
161
+ ```ts
162
+ // app.module.ts
163
+ import { Module } from '@nestjs/common';
164
+ import { McpModule } from 'mcp-expose/nestjs';
165
+
166
+ @Module({
167
+ imports: [
168
+ McpModule.forRoot({
169
+ name: 'orders-api', // shown to the AI client
170
+ version: '1.0.0',
171
+ instructions: 'Tools for looking up and placing orders.',
172
+ // path: 'mcp', // default endpoint: /mcp
173
+ // guards: [JwtAuthGuard], // protect the MCP endpoint itself (tools/list too)
174
+ }),
175
+ OrdersModule,
176
+ ],
177
+ })
178
+ export class AppModule {}
179
+ ```
180
+
181
+ **Step 2: decorate the endpoints you want to expose:**
182
+
183
+ ```ts
184
+ // orders.controller.ts
185
+ import { McpTool } from 'mcp-expose/nestjs';
186
+
187
+ export class CreateOrderDto {
188
+ @IsString() sku!: string;
189
+ @IsInt() @Min(1) @Max(10) quantity!: number;
190
+ @IsString() @IsOptional() note?: string;
191
+ }
192
+
193
+ @Controller('orders')
194
+ @UseGuards(JwtAuthGuard) // ← still enforced for every tool call
195
+ export class OrdersController {
196
+ @Get(':id')
197
+ @McpTool({ description: 'Get one order by its id.' })
198
+ findOne(@Param('id', ParseIntPipe) id: number) { … }
199
+
200
+ @Post()
201
+ @McpTool({ name: 'create_order', description: 'Create an order. quantity must be 1-10.' })
202
+ create(@Body() dto: CreateOrderDto) { … } // ← input schema generated from the DTO
203
+
204
+ @Post(':id/refund') // ← no @McpTool: invisible to agents
205
+ refund(@Param('id') id: string) { … }
206
+ }
207
+ ```
208
+
209
+ The generated tool input for `create_order`:
210
+
211
+ ```json
212
+ {
213
+ "type": "object",
214
+ "properties": {
215
+ "sku": { "type": "string" },
216
+ "quantity": { "type": "integer", "minimum": 1, "maximum": 10 },
217
+ "note": { "type": "string" }
218
+ },
219
+ "required": ["sku", "quantity"]
220
+ }
221
+ ```
222
+
223
+ **Loading options from configuration:** use `forRootAsync`. `path` and `guards` are passed directly,
224
+ because they define the MCP controller:
225
+
226
+ ```ts
227
+ McpModule.forRootAsync({
228
+ imports: [ConfigModule],
229
+ inject: [ConfigService],
230
+ useFactory: (config: ConfigService) => ({ name: config.get('APP_NAME'), version: config.get('APP_VERSION') }),
231
+ guards: [JwtAuthGuard],
232
+ });
233
+ ```
234
+
235
+ **Step 3: bootstrap as usual:**
236
+
237
+ ```ts
238
+ // main.ts
239
+ const app = await NestFactory.create(AppModule);
240
+ app.setGlobalPrefix('api', { exclude: ['mcp'] }); // optional: keep MCP at /mcp
241
+ app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
242
+ await app.listen(3000); // MCP: http://localhost:3000/mcp
243
+ ```
244
+
245
+ Notes
246
+
247
+ - Default tool names are `<controller>_<method>` in snake_case, for example `orders_find_one`. Set `name` to override.
248
+ - The global prefix (`/api`) and URI versioning (`/v1`) are detected automatically. The MCP endpoint is version-neutral.
249
+ - Works with `@nestjs/platform-express` and `@nestjs/platform-fastify`.
250
+ - Inject `McpService` to add tools at runtime: `mcpService.server.addTool(defineTool({...}))`.
251
+ - Global guards also apply to `/mcp`. If you use a global JWT guard, MCP clients must send a token, which is usually what you want. Mark the endpoint public with your own decorator mechanism if not.
252
+
253
+ ### Express
254
+
255
+ ```ts
256
+ import express from 'express';
257
+ import { z } from 'zod';
258
+ import { mcpTool, mountMcp } from 'mcp-expose/express';
259
+
260
+ const app = express();
261
+ app.use(express.json());
262
+ app.set('trust proxy', 'loopback'); // req.ip = real agent IP for tool calls (rate limiting)
263
+
264
+ // Step 1: add mcpTool() as the FIRST handler of the routes to expose
265
+ app.get(
266
+ '/products',
267
+ mcpTool({
268
+ name: 'search_products',
269
+ description: 'Search the product catalog by name.',
270
+ query: { type: 'object', properties: { q: { type: 'string' } } },
271
+ }),
272
+ searchProducts,
273
+ );
274
+
275
+ app.post(
276
+ '/orders',
277
+ mcpTool({
278
+ name: 'create_order',
279
+ description: 'Place an order.',
280
+ body: z.object({ productId: z.string(), quantity: z.number().int().min(1) }), // zod works too
281
+ }),
282
+ requireAuth,
283
+ rateLimit,
284
+ createOrder,
285
+ );
286
+
287
+ // Step 2: mount the endpoint (before or after the routes)
288
+ mountMcp(app, { name: 'shop-api', version: '1.0.0' });
289
+
290
+ app.listen(3000); // MCP: http://localhost:3000/mcp
291
+ ```
292
+
293
+ **Routers mounted with a path:** Express 5 does not record mount paths, so pass them explicitly.
294
+ Express 4 detects them automatically.
295
+
296
+ ```ts
297
+ const api = express.Router();
298
+ api.get('/orders', mcpTool({ description: 'List orders' }), listOrders);
299
+ app.use('/api', api);
300
+
301
+ mountMcp(app, { name: 'shop-api', routers: { '/api': api } });
302
+ ```
303
+
304
+ **No-touch mode:** expose routes without editing them:
305
+
306
+ ```ts
307
+ mountMcp(app, {
308
+ name: 'shop-api',
309
+ routes: [{ method: 'GET', path: '/orders/:id', description: 'Get an order' }],
310
+ });
311
+ ```
312
+
313
+ ### Fastify
314
+
315
+ ```ts
316
+ import Fastify from 'fastify';
317
+ import { fastifyMcp } from 'mcp-expose/fastify';
318
+
319
+ const app = Fastify();
320
+
321
+ // Step 1: register the plugin BEFORE your routes (it listens to onRoute)
322
+ await app.register(fastifyMcp, { name: 'todo-api', version: '1.0.0' });
323
+
324
+ // Step 2: add `config.mcp` to routes. Their JSON `schema` becomes the tool schema.
325
+ app.post(
326
+ '/todos',
327
+ {
328
+ schema: {
329
+ body: {
330
+ type: 'object',
331
+ properties: { title: { type: 'string', minLength: 1, description: 'What needs doing' } },
332
+ required: ['title'],
333
+ },
334
+ },
335
+ config: { mcp: { name: 'add_todo', description: 'Add a todo item.' } }, // or `mcp: true`
336
+ },
337
+ addTodo,
338
+ );
339
+
340
+ await app.listen({ port: 3000 }); // MCP: http://localhost:3000/mcp
341
+ ```
342
+
343
+ Protect the MCP endpoint itself with Fastify hooks: `register(fastifyMcp, { name, routeOptions: { onRequest: app.authenticate } })`.
344
+ The server is available as `app.mcpServer`.
345
+
346
+ ### Koa
347
+
348
+ ```ts
349
+ import Koa from 'koa';
350
+ import Router from '@koa/router';
351
+ import { mcpTool, mountMcp } from 'mcp-expose/koa';
352
+
353
+ const app = new Koa();
354
+ const router = new Router({ prefix: '/api' });
355
+
356
+ // Step 1: mark routes
357
+ router.get('/weather/:city', mcpTool({ description: 'Current weather for a city.' }), getWeather);
358
+
359
+ // Step 2: mount the endpoint (before your routers) and list the routers to scan
360
+ mountMcp(app, { name: 'weather-api', routers: [router] });
361
+ app.use(router.routes());
362
+
363
+ app.listen(3000); // MCP: http://localhost:3000/mcp
364
+ ```
365
+
366
+ `koaMcp(options)` returns the same endpoint as a plain middleware, for use with `koa-mount` or `koa-compose`.
367
+ Set `app.proxy = true` if your rate limiter keys on `ctx.ip`, so the forwarded agent IP is used.
368
+
369
+ **Koa 1** (generator middleware, koa-router 5): use the legacy helpers.
370
+
371
+ ```js
372
+ const { koaMcpLegacy, mcpToolLegacy } = require('mcp-expose/koa');
373
+
374
+ router.get('/weather/:city', mcpToolLegacy({ description: 'Current weather for a city.' }), getWeather);
375
+ app.use(koaMcpLegacy({ name: 'weather-api', routers: [router] }));
376
+ app.use(router.routes());
377
+ ```
378
+
379
+ ### Hono
380
+
381
+ Works on Node, Bun, Deno, Cloudflare Workers and Vercel Edge. Tool calls use `app.request()`, so no network hop is involved.
382
+
383
+ ```ts
384
+ import { Hono } from 'hono';
385
+ import { z } from 'zod';
386
+ import { mcpTool, mountMcp } from 'mcp-expose/hono';
387
+
388
+ const app = new Hono();
389
+
390
+ app.post(
391
+ '/notes',
392
+ mcpTool({ name: 'create_note', description: 'Save a note.', body: z.object({ text: z.string() }) }),
393
+ bearerAuth({ token }),
394
+ async (c) => c.json(await saveNote(await c.req.json()), 201),
395
+ );
396
+
397
+ mountMcp(app, { name: 'notes-api' });
398
+
399
+ export default app; // MCP: https://<your-worker>/mcp
400
+ ```
401
+
402
+ ### AdonisJS
403
+
404
+ Works with AdonisJS 6 and 7. Import `mcp-expose/adonisjs` in `start/routes.ts`. That adds a `.mcp()` method to
405
+ routes, next to `.as()` and `.use()`:
406
+
407
+ ```ts
408
+ // start/routes.ts
409
+ import router from '@adonisjs/core/services/router';
410
+ import { mountMcp } from 'mcp-expose/adonisjs';
411
+ import { middleware } from '#start/kernel';
412
+ import { createOrderValidator } from '#validators/order';
413
+
414
+ const OrdersController = () => import('#controllers/orders_controller');
415
+
416
+ router
417
+ .group(() => {
418
+ // Step 1: mark routes. Group prefixes and middleware (auth, throttle) all apply.
419
+ router.get('orders/:id', [OrdersController, 'show']).mcp({ description: 'Get an order by id' });
420
+
421
+ // A VineJS 4 validator (AdonisJS 7) can be passed as the schema directly
422
+ router
423
+ .post('orders', [OrdersController, 'store'])
424
+ .mcp({ name: 'create_order', description: 'Place an order', body: createOrderValidator });
425
+ })
426
+ .prefix('/api/v1')
427
+ .use(middleware.auth());
428
+
429
+ // Step 2: mount the endpoint
430
+ mountMcp(router, { name: 'shop-api' });
431
+ // MCP: http://localhost:3333/mcp
432
+ ```
433
+
434
+ Notes
435
+
436
+ - Validation errors from `request.validateUsing()` (HTTP 422) are returned to the agent so it can correct itself.
437
+ - VineJS 3 (AdonisJS 6) cannot export JSON Schema, so pass `body` as a JSON Schema object there.
438
+ - Protect the MCP endpoint itself with `configureRoute`: `mountMcp(router, { name, configureRoute: (route) => route.use(middleware.auth()) })`.
439
+ - Resource routes: mark individual actions with `router.resource('posts', PostsController).tap('show', (route) => route.mcp({ ... }))`.
440
+ - If you use the web starter kit, exclude the MCP path from CSRF protection (`config/shield.ts`, `csrf.exceptRoutes`).
441
+
442
+ ### Any API via OpenAPI (standalone gateway)
443
+
444
+ Put an MCP gateway in front of **any** HTTP API, whatever language it is written in, using its OpenAPI 3 document.
445
+ By default only operations marked `x-mcp: true` are exposed. You can also pass an `include` filter.
446
+
447
+ ```ts
448
+ import express from 'express';
449
+ import { createFetchDispatcher, toolsFromOpenApi } from 'mcp-expose';
450
+ import { mountMcp } from 'mcp-expose/express';
451
+
452
+ const spec = await fetch('https://api.example.com/openapi.json').then((r) => r.json());
453
+ const tools = toolsFromOpenApi(spec, createFetchDispatcher({ baseUrl: 'https://api.example.com' }), {
454
+ include: ({ method }) => method === 'get', // e.g. only read-only operations
455
+ });
456
+
457
+ const app = express();
458
+ mountMcp(app, { name: 'example-gateway', tools });
459
+ app.listen(3000);
460
+ ```
461
+
462
+ This also works with `@nestjs/swagger`, `@fastify/swagger`, tsoa and hono-openapi documents. Pass a dereferenced document, because `$ref`s are not resolved.
463
+
464
+ ---
465
+
466
+ ## Connect an AI client
467
+
468
+ Start your app, then point a client at `http://localhost:3000/mcp`. Pass the same credentials a normal API
469
+ client would use. They are forwarded to your routes.
470
+
471
+ **Claude Code**
472
+
473
+ ```bash
474
+ claude mcp add --transport http shop-api http://localhost:3000/mcp \
475
+ --header "Authorization: Bearer <token>"
476
+ ```
477
+
478
+ **Cursor** (`~/.cursor/mcp.json` or `.cursor/mcp.json`)
479
+
480
+ ```json
481
+ {
482
+ "mcpServers": {
483
+ "shop-api": {
484
+ "url": "http://localhost:3000/mcp",
485
+ "headers": { "Authorization": "Bearer <token>" }
486
+ }
487
+ }
488
+ }
489
+ ```
490
+
491
+ **VS Code (Copilot agent mode)** (`.vscode/mcp.json`)
492
+
493
+ ```json
494
+ {
495
+ "servers": {
496
+ "shop-api": {
497
+ "type": "http",
498
+ "url": "http://localhost:3000/mcp",
499
+ "headers": { "Authorization": "Bearer ${input:token}" }
500
+ }
501
+ },
502
+ "inputs": [{ "id": "token", "type": "promptString", "description": "API token", "password": true }]
503
+ }
504
+ ```
505
+
506
+ **Claude Desktop.** For a deployed server, add it under _Settings → Connectors_ using its public URL.
507
+ For a local server, bridge it with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) in `claude_desktop_config.json`:
508
+
509
+ ```json
510
+ {
511
+ "mcpServers": {
512
+ "shop-api": {
513
+ "command": "npx",
514
+ "args": ["mcp-remote", "http://localhost:3000/mcp", "--header", "Authorization: Bearer ${API_TOKEN}"],
515
+ "env": { "API_TOKEN": "<token>" }
516
+ }
517
+ }
518
+ }
519
+ ```
520
+
521
+ **Debug with MCP Inspector**
522
+
523
+ ```bash
524
+ npx @modelcontextprotocol/inspector
525
+ # Transport: Streamable HTTP, URL: http://localhost:3000/mcp
526
+ ```
527
+
528
+ **curl smoke test**
529
+
530
+ ```bash
531
+ curl -s localhost:3000/mcp -H 'content-type: application/json' \
532
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
533
+ ```
534
+
535
+ ---
536
+
537
+ ## Defining tool inputs (schemas)
538
+
539
+ The agent sees **one flat object** of arguments. mcp-expose maps each argument back to the right place:
540
+
541
+ | Argument | Sent as |
542
+ | ---------------------------------------------------- | ----------------------------------------- |
543
+ | name matches a path placeholder (`:id`, `{id}`) | path segment (URL-encoded) |
544
+ | declared in `query` | query string |
545
+ | declared in `body` | JSON body property |
546
+ | undeclared, route is `GET`/`DELETE` | query string |
547
+ | undeclared, route is `POST`/`PUT`/`PATCH` | JSON body property |
548
+ | `body` is a non-object schema (for example an array) | the whole body, under the `body` argument |
549
+
550
+ Every schema option accepts:
551
+
552
+ - a plain **JSON Schema** object,
553
+ - a **zod 4** schema (`z.object({...})`),
554
+ - any **Standard Schema** library with JSON Schema export (valibot, arktype, …). These are also validated before the request is sent.
555
+
556
+ Sources, from highest to lowest precedence:
557
+
558
+ 1. `input` in the tool options: the full schema, used as is.
559
+ 2. `params` / `query` / `body` in the tool options.
560
+ 3. Framework metadata: NestJS DTOs and `@Param`/`@Query` types, or Fastify route `schema`.
561
+ 4. Path placeholders, as string parameters.
562
+
563
+ Your app's own validation always runs as well. The schema tells the agent what to send, and your API decides what it accepts.
564
+
565
+ ## Configuration reference
566
+
567
+ ### Server options (all adapters)
568
+
569
+ | Option | Type | Default | Description |
570
+ | ------------------ | --------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
571
+ | `name` | `string` | required | Server name shown to clients. |
572
+ | `version` | `string` | `'1.0.0'` | Server version. |
573
+ | `instructions` | `string` | none | Guidance for the model on how to use the tools together. |
574
+ | `path` | `string` | `'/mcp'` | Endpoint path. |
575
+ | `allowedOrigins` | `string[] \| '*'` | `[]` | Browser origins allowed to call the endpoint. Requests without `Origin` (CLIs, IDEs, servers) are always allowed. |
576
+ | `forwardHeaders` | `string[]` | `['authorization','cookie','x-api-key','accept-language']` | Headers copied from the MCP request to the internal API call. |
577
+ | `maxResponseChars` | `number` | `100000` | Longer API responses are truncated before reaching the model. |
578
+ | `tools` | `McpToolDefinition[]` | `[]` | Extra hand-written tools. |
579
+ | `baseUrl` | `string` | loopback | _Express/Koa/Nest/Adonis._ Where internal calls go. Set it for HTTPS with self-signed certs, unix sockets, or a separate API host. |
580
+ | `routes` | `{method,path,...}[]` | `[]` | _Express/Koa/Hono._ Expose routes without editing them. |
581
+ | `routers` | see guide | none | _Express:_ `{ '/prefix': router }`. _Koa:_ `[router]`. |
582
+ | `middleware` | `Middleware[]` | `[]` | _Express._ Middleware in front of `/mcp`, such as auth. |
583
+ | `guards` | `CanActivate[]` | `[]` | _NestJS._ Guards on the MCP controller. |
584
+ | `pathPrefix` | `string` | none | _NestJS._ Extra prefix for tool routes. Global prefix and URI versioning are automatic. |
585
+ | `routeOptions` | `object` | none | _Fastify._ Extra route options for `/mcp`, such as `onRequest` hooks. |
586
+ | `configureRoute` | `(route) => void` | none | _AdonisJS._ Configure the MCP route, e.g. add middleware. |
587
+
588
+ ### Tool options (`@McpTool()`, `mcpTool()`, `config.mcp`, `.mcp()`)
589
+
590
+ | Option | Description |
591
+ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
592
+ | `name` | Tool name (`[A-Za-z0-9_-]`, max 64). Default is derived from the route, e.g. `get_users_by_id`. Agents call tools by name, so set it explicitly and keep it stable. |
593
+ | `description` | **The most important field.** Tells the model what the tool does and when to use it. |
594
+ | `title` | Human-friendly display name. |
595
+ | `input` / `params` / `query` / `body` | Schemas, see [above](#defining-tool-inputs-schemas). |
596
+ | `annotations` | MCP hints: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`. Defaults come from the HTTP method (GET → read-only, DELETE → destructive). |
597
+ | `headers` | Static headers added to the internal request. |
598
+
599
+ Each internal request also carries `X-Mcp-Tool: <tool name>`, so you can log or meter agent traffic separately.
600
+
601
+ ## Custom (non-HTTP) tools
602
+
603
+ Not everything needs a route:
604
+
605
+ ```ts
606
+ import { defineTool } from 'mcp-expose';
607
+ import { z } from 'zod';
608
+
609
+ const convert = defineTool({
610
+ name: 'convert_currency',
611
+ description: "Convert an amount between currencies using today's rate.",
612
+ input: z.object({ amount: z.number(), from: z.string().length(3), to: z.string().length(3) }),
613
+ handler: async ({ amount, from, to }, ctx) => ({ result: await fx.convert(amount, from, to) }),
614
+ });
615
+
616
+ mountMcp(app, { name: 'shop-api', tools: [convert] });
617
+ ```
618
+
619
+ Handlers can return a string, any JSON value, or a full MCP `ToolResult`. `ctx.headers` holds the MCP request's headers, so you can authenticate there too.
620
+
621
+ ## Security checklist
622
+
623
+ - **Opt-in only.** Nothing is exposed unless you mark it. Review marked routes the way you review a public API, because an LLM can call them with any arguments.
624
+ - **Use per-user credentials.** Clients send their own token, the token is forwarded, and your guards authorise the call. Avoid one shared super-token.
625
+ - **Protect discovery too** if tool names are sensitive (`guards`, `middleware`, `routeOptions`).
626
+ - **Rate limits and IPs:** the agent IP is sent as `X-Forwarded-For`. For loopback adapters, trust loopback only: Express `app.set('trust proxy', 'loopback')`, Koa `app.proxy = true` behind a proxy that overwrites the header, Nest (Express) `app.set('trust proxy', 'loopback')`. Fastify `inject()` sets the IP directly.
627
+ - **Browser access:** keep `allowedOrigins` empty unless a browser app must call `/mcp` directly.
628
+ - **Annotations are hints, not security.** A `readOnlyHint` never prevents a call. Enforce permissions in your API.
629
+ - **Destructive actions:** clients may use `destructiveHint` to decide when to ask the user for confirmation. Keep it accurate, and require stronger auth for dangerous routes.
630
+ - **Response size:** tune `maxResponseChars`, and prefer endpoints that paginate.
631
+
632
+ ## Writing tools agents use well
633
+
634
+ - Describe **when** to use the tool, not only what it does: _"Search products by name. Use this before `create_order` to find a valid `productId`."_
635
+ - Document units, formats and limits in schema `description`s (`"price in cents"`, `"ISO 8601 date"`).
636
+ - Prefer a few task-shaped tools (`search_orders`) over many CRUD primitives.
637
+ - Return clear 4xx messages. They go straight to the model, which uses them to retry correctly.
638
+ - Use `instructions` on the server for cross-tool workflow hints.
639
+
640
+ ## Limitations and roadmap
641
+
642
+ Current scope (1.x):
643
+
644
+ - Stateless Streamable HTTP with JSON responses. No server-initiated SSE stream or sessions. The spec allows this, and it keeps the server horizontally scalable.
645
+ - Tools only. `resources/list` and `prompts/list` return empty lists.
646
+ - NestJS: URI versioning is detected. Header and media-type versioning need `headers` on the tool.
647
+ - Express 5 routers mounted with a path must be listed in `routers`.
648
+ - Path parameters cannot be empty, `.` or `..`. These are rejected so an agent cannot reach routes that were not exposed.
649
+
650
+ Planned (non-breaking, 1.x minor releases):
651
+
652
+ - OAuth 2.1 protected-resource metadata (RFC 9728) helpers for remote MCP auth, and per-user tool lists
653
+ - Next.js route handlers, Hapi and Elysia adapters
654
+ - Structured output schemas, and binary/file responses
655
+ - Streaming long-running responses over SSE
656
+ - CLI to preview generated tools (`npx mcp-expose inspect`)
657
+ - Resources from GET routes, and prompts
658
+
659
+ Contributions are welcome. See [Development](#development).
660
+
661
+ ## Development
662
+
663
+ ```bash
664
+ npm install
665
+ npm test # vitest: core + all six adapters (real servers, real HTTP)
666
+ npm run test:e2e # pack → install into 18 fresh framework projects → official MCP SDK client
667
+ npm run typecheck
668
+ npm run build # ESM + CJS + .d.ts into dist/
669
+
670
+ # run an example (after npm run build)
671
+ npm run example:express # or example:fastify / example:koa / example:hono / example:nestjs
672
+ ```
673
+
674
+ Project layout:
675
+
676
+ ```
677
+ src/
678
+ core/ framework-agnostic: MCP JSON-RPC server, route→tool mapping, schemas, dispatchers
679
+ express/ mcpTool() + mountMcp()
680
+ nestjs/ @McpTool() + McpModule + DTO → JSON Schema
681
+ fastify/ fastifyMcp plugin (config.mcp)
682
+ koa/ mcpTool() + mountMcp() / koaMcp() (+ Koa 1 legacy helpers)
683
+ hono/ mcpTool() + mountMcp()
684
+ adonisjs/ .mcp() route macro + mountMcp()
685
+ examples/ runnable apps for every framework + an OpenAPI gateway
686
+ e2e/ developer-style end-to-end tests (one project per framework/version)
687
+ test/ one shared behavioural contract, verified against every adapter
688
+ ```
689
+
690
+ Adding an adapter: find the marked routes, then call `createRouteTool(route, options, dispatcher)` and
691
+ `server.handleHttp()`. Reuse `test/helpers.ts#assertAdapterContract` to test it.
692
+
693
+ ## Versioning and support
694
+
695
+ mcp-expose follows [Semantic Versioning](https://semver.org). Older major versions keep receiving fixes on
696
+ maintenance branches, published under `latest-<major>` dist-tags (for example `npm i mcp-expose@latest-3`),
697
+ so a fix to an old major never changes what `npm i mcp-expose` installs. See the
698
+ [support policy](SECURITY.md#supported-versions) and [RELEASING.md](RELEASING.md).
699
+
700
+ ## Contributing
701
+
702
+ Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) and the
703
+ [Code of Conduct](CODE_OF_CONDUCT.md). Report security issues privately as described in [SECURITY.md](SECURITY.md).
704
+ Release notes are in [CHANGELOG.md](CHANGELOG.md).
705
+
706
+ ## License
707
+
708
+ [MIT](LICENSE) © Nitin Kachhadiya