mcp-expose 1.1.1 → 1.1.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/README.md +58 -9
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.1.2] - 2026-09-28
10
+
11
+ ### Documentation
12
+
13
+ - README: every framework guide example (NestJS, Express, Fastify, Koa, Hono, AdonisJS, OpenAPI gateway) now shows
14
+ `transformResponse`; the version compatibility section says what each framework version is verified for (including
15
+ OAuth and `transformResponse`), the `transformResponse` section names the marker for every framework, and
16
+ `ctx.callRoute()` documents each framework's path prefixes.
17
+
18
+ ### Tests
19
+
20
+ - `transformResponse` with `ctx.callRoute()` is checked end to end on every supported framework version (NestJS 10-12,
21
+ Express 3-5, Fastify 3-5, Koa 1-3, Hono 2-4, AdonisJS 6-7, OpenAPI gateway), with and without OAuth.
22
+
9
23
  ## [1.1.1] - 2026-09-28
10
24
 
11
25
  ### Added
@@ -68,7 +82,8 @@ options, default tool naming) now follows Semantic Versioning.
68
82
  - Path parameters that are empty, `.` or `..` are rejected, so an agent cannot use them to reach routes
69
83
  that were never exposed as tools (for example `/orders/../admin`).
70
84
 
71
- [Unreleased]: https://github.com/NITINKACHHADIYA/Node-MCP/compare/v1.1.1...HEAD
85
+ [Unreleased]: https://github.com/NITINKACHHADIYA/Node-MCP/compare/v1.1.2...HEAD
86
+ [1.1.2]: https://github.com/NITINKACHHADIYA/Node-MCP/releases/tag/v1.1.2
72
87
  [1.1.1]: https://github.com/NITINKACHHADIYA/Node-MCP/releases/tag/v1.1.1
73
88
  [1.1.0]: https://github.com/NITINKACHHADIYA/Node-MCP/releases/tag/v1.1.0
74
89
  [1.0.0]: https://github.com/NITINKACHHADIYA/Node-MCP/releases/tag/v1.0.0
package/README.md CHANGED
@@ -125,7 +125,9 @@ Tools are discovered **lazily on the first MCP request**, so the order you regis
125
125
 
126
126
  mcp-expose supports the **current major version of each framework and the two before it**. Every row
127
127
  below runs in CI as a real project (see [`e2e/`](e2e/README.md)): the packed library is installed from its
128
- tarball and the app is driven by the official MCP SDK client, on Node.js 20, 22 and 24.
128
+ tarball and the app is driven by the official MCP SDK client, on Node.js 20, 22 and 24. Each row is checked for tool
129
+ discovery, auth forwarding, the app's own validation errors, [OAuth](#oauth-sign-in-from-ai-clients) and
130
+ [`transformResponse`](#shaping-and-enriching-responses), with and without the `oauth` option.
129
131
 
130
132
  | Framework | Supported majors | Notes |
131
133
  | --------- | ---------------- | ------------------------------------------------------------------------------------ |
@@ -197,7 +199,16 @@ export class CreateOrderDto {
197
199
  @UseGuards(JwtAuthGuard) // ← still enforced for every tool call
198
200
  export class OrdersController {
199
201
  @Get(':id')
200
- @McpTool({ description: 'Get one order by its id.' })
202
+ @McpTool({
203
+ description: 'Get one order by its id.',
204
+ // Optional: shape what the agent sees. Hide internal fields, add the customer from another route.
205
+ transformResponse: async (res, ctx) => {
206
+ if (!res.ok) return undefined; // keep the default error result
207
+ const { internalNotes, ...order } = res.json as Order;
208
+ const customer = await ctx.callRoute({ path: '/api/customers/:id', params: { id: order.customerId } });
209
+ return { ...order, customer: customer.json };
210
+ },
211
+ })
201
212
  findOne(@Param('id', ParseIntPipe) id: number) { … }
202
213
 
203
214
  @Post()
@@ -271,6 +282,9 @@ app.get(
271
282
  name: 'search_products',
272
283
  description: 'Search the product catalog by name.',
273
284
  query: { type: 'object', properties: { q: { type: 'string' } } },
285
+ // Optional: only send the fields the agent needs
286
+ transformResponse: (res) =>
287
+ res.ok ? (res.json as { items: Product[] }).items.map(({ id, name, price }) => ({ id, name, price })) : undefined,
274
288
  }),
275
289
  searchProducts,
276
290
  );
@@ -335,7 +349,14 @@ app.post(
335
349
  required: ['title'],
336
350
  },
337
351
  },
338
- config: { mcp: { name: 'add_todo', description: 'Add a todo item.' } }, // or `mcp: true`
352
+ config: {
353
+ mcp: {
354
+ name: 'add_todo',
355
+ description: 'Add a todo item.',
356
+ // Optional: shape the response the agent sees
357
+ transformResponse: (res) => (res.ok ? { created: true, id: (res.json as Todo).id } : undefined),
358
+ },
359
+ }, // or `mcp: true`
339
360
  },
340
361
  addTodo,
341
362
  );
@@ -357,7 +378,15 @@ const app = new Koa();
357
378
  const router = new Router({ prefix: '/api' });
358
379
 
359
380
  // Step 1: mark routes
360
- router.get('/weather/:city', mcpTool({ description: 'Current weather for a city.' }), getWeather);
381
+ router.get(
382
+ '/weather/:city',
383
+ mcpTool({
384
+ description: 'Current weather for a city.',
385
+ // Optional: add a hint for the agent
386
+ transformResponse: (res) => (res.ok ? { ...(res.json as object), unit: 'celsius' } : undefined),
387
+ }),
388
+ getWeather,
389
+ );
361
390
 
362
391
  // Step 2: mount the endpoint (before your routers) and list the routers to scan
363
392
  mountMcp(app, { name: 'weather-api', routers: [router] });
@@ -392,7 +421,13 @@ const app = new Hono();
392
421
 
393
422
  app.post(
394
423
  '/notes',
395
- mcpTool({ name: 'create_note', description: 'Save a note.', body: z.object({ text: z.string() }) }),
424
+ mcpTool({
425
+ name: 'create_note',
426
+ description: 'Save a note.',
427
+ body: z.object({ text: z.string() }),
428
+ // Optional: shape the response the agent sees
429
+ transformResponse: (res) => (res.ok ? { saved: true, id: (res.json as Note).id } : undefined),
430
+ }),
396
431
  bearerAuth({ token }),
397
432
  async (c) => c.json(await saveNote(await c.req.json()), 201),
398
433
  );
@@ -419,7 +454,16 @@ const OrdersController = () => import('#controllers/orders_controller');
419
454
  router
420
455
  .group(() => {
421
456
  // Step 1: mark routes. Group prefixes and middleware (auth, throttle) all apply.
422
- router.get('orders/:id', [OrdersController, 'show']).mcp({ description: 'Get an order by id' });
457
+ router.get('orders/:id', [OrdersController, 'show']).mcp({
458
+ description: 'Get an order by id',
459
+ // Optional: add the customer from another route (same auth). Paths include the group prefix.
460
+ transformResponse: async (res, ctx) => {
461
+ if (!res.ok) return undefined;
462
+ const order = res.json as Order;
463
+ const customer = await ctx.callRoute({ path: '/api/v1/customers/:id', params: { id: order.customerId } });
464
+ return { ...order, customer: customer.json };
465
+ },
466
+ });
423
467
 
424
468
  // A VineJS 4 validator (AdonisJS 7) can be passed as the schema directly
425
469
  router
@@ -455,6 +499,8 @@ import { mountMcp } from 'mcp-expose/express';
455
499
  const spec = await fetch('https://api.example.com/openapi.json').then((r) => r.json());
456
500
  const tools = toolsFromOpenApi(spec, createFetchDispatcher({ baseUrl: 'https://api.example.com' }), {
457
501
  include: ({ method }) => method === 'get', // e.g. only read-only operations
502
+ // Optional: shape responses of the generated tools (ctx.tool is the tool name)
503
+ transformResponse: (res, ctx) => (ctx.tool === 'get_user' && res.ok ? redact(res.json) : undefined),
458
504
  });
459
505
 
460
506
  const app = express();
@@ -670,8 +716,9 @@ routes. The route stays a route tool; there is no need to rewrite it as a custom
670
716
  findOne(@Param('id') id: string) { ... }
671
717
  ```
672
718
 
673
- It works the same with every marker: `mcpTool({ transformResponse })`, `config: { mcp: { transformResponse } }`,
674
- `.mcp({ transformResponse })`, and `toolsFromOpenApi(doc, dispatch, { transformResponse })`.
719
+ It works the same with every marker and every supported framework version: `@McpTool({ transformResponse })` (NestJS),
720
+ `mcpTool({ transformResponse })` (Express, Koa, Hono; `mcpToolLegacy()` on Koa 1), `config: { mcp: { transformResponse } }`
721
+ (Fastify), `.mcp({ transformResponse })` (AdonisJS) and `toolsFromOpenApi(doc, dispatch, { transformResponse })`.
675
722
 
676
723
  **What the hook receives**
677
724
 
@@ -681,7 +728,9 @@ It works the same with every marker: `mcpTool({ transformResponse })`, `config:
681
728
  - `ctx.defaultResult()`: the result mcp-expose would return without the hook.
682
729
  - `ctx.callRoute({ method, path, params, query, body, headers })`: calls another route of the same app.
683
730
  - It forwards the same credentials (`Authorization`, cookies) and runs through the same middleware, guards and validation.
684
- - `path` is the full path as your server sees it, including any global prefix or router mount path. Fill `:id` / `{id}`
731
+ - `path` is the full path as your server sees it, including every prefix: NestJS global prefix and URI version
732
+ (`/api/v1/...`), Express router mount path, Koa router `prefix`, Hono `app.route()` mount, AdonisJS group `.prefix()`.
733
+ `ctx.request.path` shows the full path of the tool's own route. Fill `:id` / `{id}`
685
734
  placeholders from `params`: values are URL-encoded, and `.` or `..` are rejected, so agent input can't redirect the call.
686
735
 
687
736
  **What it returns**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-expose",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Turn the HTTP routes of your existing Node.js app (NestJS, Express, Fastify, Koa, Hono, AdonisJS) into MCP tools for AI agents, reusing your auth, validation and rate limits.",
5
5
  "keywords": [
6
6
  "mcp",