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.
- package/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.md +708 -0
- package/dist/adonisjs/index.cjs +708 -0
- package/dist/adonisjs/index.cjs.map +1 -0
- package/dist/adonisjs/index.d.cts +68 -0
- package/dist/adonisjs/index.d.ts +68 -0
- package/dist/adonisjs/index.js +684 -0
- package/dist/adonisjs/index.js.map +1 -0
- package/dist/express/index.cjs +789 -0
- package/dist/express/index.cjs.map +1 -0
- package/dist/express/index.d.cts +61 -0
- package/dist/express/index.d.ts +61 -0
- package/dist/express/index.js +763 -0
- package/dist/express/index.js.map +1 -0
- package/dist/fastify/index.cjs +660 -0
- package/dist/fastify/index.cjs.map +1 -0
- package/dist/fastify/index.d.cts +42 -0
- package/dist/fastify/index.d.ts +42 -0
- package/dist/fastify/index.js +635 -0
- package/dist/fastify/index.js.map +1 -0
- package/dist/hono/index.cjs +656 -0
- package/dist/hono/index.cjs.map +1 -0
- package/dist/hono/index.d.cts +44 -0
- package/dist/hono/index.d.ts +44 -0
- package/dist/hono/index.js +630 -0
- package/dist/hono/index.js.map +1 -0
- package/dist/index.cjs +720 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +81 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +686 -0
- package/dist/index.js.map +1 -0
- package/dist/koa/index.cjs +739 -0
- package/dist/koa/index.cjs.map +1 -0
- package/dist/koa/index.d.cts +68 -0
- package/dist/koa/index.d.ts +68 -0
- package/dist/koa/index.js +709 -0
- package/dist/koa/index.js.map +1 -0
- package/dist/nestjs/index.cjs +1117 -0
- package/dist/nestjs/index.cjs.map +1 -0
- package/dist/nestjs/index.d.cts +104 -0
- package/dist/nestjs/index.d.ts +104 -0
- package/dist/nestjs/index.js +1079 -0
- package/dist/nestjs/index.js.map +1 -0
- package/dist/server-fzkb-wJo.d.cts +194 -0
- package/dist/server-fzkb-wJo.d.ts +194 -0
- package/package.json +225 -0
package/README.md
ADDED
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
# mcp-expose
|
|
2
|
+
|
|
3
|
+
[](https://github.com/NITINKACHHADIYA/Node-MCP/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/mcp-expose)
|
|
5
|
+
[](https://www.npmjs.com/package/mcp-expose)
|
|
6
|
+
[](package.json)
|
|
7
|
+
[](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
|