mcp-expose 1.1.0 → 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.
- package/CHANGELOG.md +33 -1
- package/README.md +143 -34
- package/dist/adonisjs/index.cjs +128 -6
- package/dist/adonisjs/index.cjs.map +1 -1
- package/dist/adonisjs/index.d.cts +1 -1
- package/dist/adonisjs/index.d.ts +1 -1
- package/dist/adonisjs/index.js +128 -6
- package/dist/adonisjs/index.js.map +1 -1
- package/dist/express/index.cjs +128 -6
- package/dist/express/index.cjs.map +1 -1
- package/dist/express/index.d.cts +1 -1
- package/dist/express/index.d.ts +1 -1
- package/dist/express/index.js +128 -6
- package/dist/express/index.js.map +1 -1
- package/dist/fastify/index.cjs +128 -6
- package/dist/fastify/index.cjs.map +1 -1
- package/dist/fastify/index.d.cts +1 -1
- package/dist/fastify/index.d.ts +1 -1
- package/dist/fastify/index.js +128 -6
- package/dist/fastify/index.js.map +1 -1
- package/dist/hono/index.cjs +128 -6
- package/dist/hono/index.cjs.map +1 -1
- package/dist/hono/index.d.cts +1 -1
- package/dist/hono/index.d.ts +1 -1
- package/dist/hono/index.js +128 -6
- package/dist/hono/index.js.map +1 -1
- package/dist/index.cjs +128 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -5
- package/dist/index.d.ts +7 -5
- package/dist/index.js +128 -31
- package/dist/index.js.map +1 -1
- package/dist/koa/index.cjs +128 -6
- package/dist/koa/index.cjs.map +1 -1
- package/dist/koa/index.d.cts +1 -1
- package/dist/koa/index.d.ts +1 -1
- package/dist/koa/index.js +128 -6
- package/dist/koa/index.js.map +1 -1
- package/dist/nestjs/index.cjs +128 -6
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.d.cts +1 -1
- package/dist/nestjs/index.d.ts +1 -1
- package/dist/nestjs/index.js +128 -6
- package/dist/nestjs/index.js.map +1 -1
- package/dist/{server-BFQ_Hdh9.d.cts → server--mQVQq3A.d.cts} +55 -2
- package/dist/{server-BRw1E2xy.d.ts → server-BPDTsT2o.d.ts} +55 -2
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,36 @@ 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
|
+
|
|
23
|
+
## [1.1.1] - 2026-09-28
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `transformResponse` hook for route tools, on every marker and adapter, plus a server-level default. It shapes what
|
|
28
|
+
the agent sees from a route's response (hide or format fields, add hints) without turning the route into a custom
|
|
29
|
+
tool. The hook receives the status, headers, body and parsed JSON, the tool arguments and the MCP call context.
|
|
30
|
+
`ctx.callRoute()` enriches the response with other routes of the app, using the same credentials and the same
|
|
31
|
+
middleware, guards and validation. Placeholders are URL-encoded, and `.` / `..` segments are rejected.
|
|
32
|
+
- `ResponseTransform`, `ResponseTransformContext` and `RouteToolResponse` types.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- A tool handler or hook that returns plain data with its own `content` array (for example a CMS post) is no longer
|
|
37
|
+
mistaken for an MCP `ToolResult`; only `content` arrays of MCP content items (`{ type: ... }`) are.
|
|
38
|
+
|
|
9
39
|
## [1.1.0] - 2026-09-27
|
|
10
40
|
|
|
11
41
|
### Added
|
|
@@ -52,6 +82,8 @@ options, default tool naming) now follows Semantic Versioning.
|
|
|
52
82
|
- Path parameters that are empty, `.` or `..` are rejected, so an agent cannot use them to reach routes
|
|
53
83
|
that were never exposed as tools (for example `/orders/../admin`).
|
|
54
84
|
|
|
55
|
-
[Unreleased]: https://github.com/NITINKACHHADIYA/Node-MCP/compare/v1.1.
|
|
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
|
|
87
|
+
[1.1.1]: https://github.com/NITINKACHHADIYA/Node-MCP/releases/tag/v1.1.1
|
|
56
88
|
[1.1.0]: https://github.com/NITINKACHHADIYA/Node-MCP/releases/tag/v1.1.0
|
|
57
89
|
[1.0.0]: https://github.com/NITINKACHHADIYA/Node-MCP/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -47,13 +47,14 @@ router.get('/orders/:id', [OrdersController, 'show']).use(middleware.auth()).mcp
|
|
|
47
47
|
6. [Connect an AI client](#connect-an-ai-client)
|
|
48
48
|
7. [OAuth: sign in from AI clients](#oauth-sign-in-from-ai-clients)
|
|
49
49
|
8. [Defining tool inputs (schemas)](#defining-tool-inputs-schemas)
|
|
50
|
-
9. [
|
|
51
|
-
10. [
|
|
52
|
-
11. [
|
|
53
|
-
12. [
|
|
54
|
-
13. [
|
|
55
|
-
14. [
|
|
56
|
-
15. [
|
|
50
|
+
9. [Shaping and enriching responses](#shaping-and-enriching-responses)
|
|
51
|
+
10. [Configuration reference](#configuration-reference)
|
|
52
|
+
11. [Custom (non-HTTP) tools](#custom-non-http-tools)
|
|
53
|
+
12. [Security checklist](#security-checklist)
|
|
54
|
+
13. [Writing tools agents use well](#writing-tools-agents-use-well)
|
|
55
|
+
14. [Limitations and roadmap](#limitations-and-roadmap)
|
|
56
|
+
15. [Development](#development)
|
|
57
|
+
16. [Versioning and support](#versioning-and-support)
|
|
57
58
|
|
|
58
59
|
---
|
|
59
60
|
|
|
@@ -124,7 +125,9 @@ Tools are discovered **lazily on the first MCP request**, so the order you regis
|
|
|
124
125
|
|
|
125
126
|
mcp-expose supports the **current major version of each framework and the two before it**. Every row
|
|
126
127
|
below runs in CI as a real project (see [`e2e/`](e2e/README.md)): the packed library is installed from its
|
|
127
|
-
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.
|
|
128
131
|
|
|
129
132
|
| Framework | Supported majors | Notes |
|
|
130
133
|
| --------- | ---------------- | ------------------------------------------------------------------------------------ |
|
|
@@ -196,7 +199,16 @@ export class CreateOrderDto {
|
|
|
196
199
|
@UseGuards(JwtAuthGuard) // ← still enforced for every tool call
|
|
197
200
|
export class OrdersController {
|
|
198
201
|
@Get(':id')
|
|
199
|
-
@McpTool({
|
|
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
|
+
})
|
|
200
212
|
findOne(@Param('id', ParseIntPipe) id: number) { … }
|
|
201
213
|
|
|
202
214
|
@Post()
|
|
@@ -270,6 +282,9 @@ app.get(
|
|
|
270
282
|
name: 'search_products',
|
|
271
283
|
description: 'Search the product catalog by name.',
|
|
272
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,
|
|
273
288
|
}),
|
|
274
289
|
searchProducts,
|
|
275
290
|
);
|
|
@@ -334,7 +349,14 @@ app.post(
|
|
|
334
349
|
required: ['title'],
|
|
335
350
|
},
|
|
336
351
|
},
|
|
337
|
-
config: {
|
|
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`
|
|
338
360
|
},
|
|
339
361
|
addTodo,
|
|
340
362
|
);
|
|
@@ -356,7 +378,15 @@ const app = new Koa();
|
|
|
356
378
|
const router = new Router({ prefix: '/api' });
|
|
357
379
|
|
|
358
380
|
// Step 1: mark routes
|
|
359
|
-
router.get(
|
|
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
|
+
);
|
|
360
390
|
|
|
361
391
|
// Step 2: mount the endpoint (before your routers) and list the routers to scan
|
|
362
392
|
mountMcp(app, { name: 'weather-api', routers: [router] });
|
|
@@ -391,7 +421,13 @@ const app = new Hono();
|
|
|
391
421
|
|
|
392
422
|
app.post(
|
|
393
423
|
'/notes',
|
|
394
|
-
mcpTool({
|
|
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
|
+
}),
|
|
395
431
|
bearerAuth({ token }),
|
|
396
432
|
async (c) => c.json(await saveNote(await c.req.json()), 201),
|
|
397
433
|
);
|
|
@@ -418,7 +454,16 @@ const OrdersController = () => import('#controllers/orders_controller');
|
|
|
418
454
|
router
|
|
419
455
|
.group(() => {
|
|
420
456
|
// Step 1: mark routes. Group prefixes and middleware (auth, throttle) all apply.
|
|
421
|
-
router.get('orders/:id', [OrdersController, 'show']).mcp({
|
|
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
|
+
});
|
|
422
467
|
|
|
423
468
|
// A VineJS 4 validator (AdonisJS 7) can be passed as the schema directly
|
|
424
469
|
router
|
|
@@ -454,6 +499,8 @@ import { mountMcp } from 'mcp-expose/express';
|
|
|
454
499
|
const spec = await fetch('https://api.example.com/openapi.json').then((r) => r.json());
|
|
455
500
|
const tools = toolsFromOpenApi(spec, createFetchDispatcher({ baseUrl: 'https://api.example.com' }), {
|
|
456
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),
|
|
457
504
|
});
|
|
458
505
|
|
|
459
506
|
const app = express();
|
|
@@ -648,30 +695,91 @@ Sources, from highest to lowest precedence:
|
|
|
648
695
|
|
|
649
696
|
Your app's own validation always runs as well. The schema tells the agent what to send, and your API decides what it accepts.
|
|
650
697
|
|
|
698
|
+
## Shaping and enriching responses
|
|
699
|
+
|
|
700
|
+
By default the agent gets the route's response as it is. Add `transformResponse` to a route tool to change
|
|
701
|
+
what the agent sees: hide internal fields, format values, add hints, or pull in related data from other
|
|
702
|
+
routes. The route stays a route tool; there is no need to rewrite it as a custom tool.
|
|
703
|
+
|
|
704
|
+
```ts
|
|
705
|
+
@Get(':id')
|
|
706
|
+
@McpTool({
|
|
707
|
+
name: 'get_order',
|
|
708
|
+
transformResponse: async (res, ctx) => {
|
|
709
|
+
if (!res.ok) return undefined; // keep the default error result
|
|
710
|
+
const { internalNotes, ...order } = res.json as Order;
|
|
711
|
+
// Another route of this app, with the same user's credentials, guards and pipes:
|
|
712
|
+
const customer = await ctx.callRoute({ path: '/api/customers/:id', params: { id: order.customerId } });
|
|
713
|
+
return { ...order, total: formatMoney(order.total), customer: customer.ok ? customer.json : null };
|
|
714
|
+
},
|
|
715
|
+
})
|
|
716
|
+
findOne(@Param('id') id: string) { ... }
|
|
717
|
+
```
|
|
718
|
+
|
|
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 })`.
|
|
722
|
+
|
|
723
|
+
**What the hook receives**
|
|
724
|
+
|
|
725
|
+
- `res`: `status`, `headers`, the raw `body`, the parsed `json` (for JSON responses) and `ok` (status below 400).
|
|
726
|
+
- `ctx.args`: the tool arguments. `ctx.request`: the internal request (method, path, query, body).
|
|
727
|
+
- `ctx.toolContext`: the MCP request's headers, client IP and, with [OAuth](#oauth-sign-in-from-ai-clients), `auth`.
|
|
728
|
+
- `ctx.defaultResult()`: the result mcp-expose would return without the hook.
|
|
729
|
+
- `ctx.callRoute({ method, path, params, query, body, headers })`: calls another route of the same app.
|
|
730
|
+
- It forwards the same credentials (`Authorization`, cookies) and runs through the same middleware, guards and validation.
|
|
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}`
|
|
734
|
+
placeholders from `params`: values are URL-encoded, and `.` or `..` are rejected, so agent input can't redirect the call.
|
|
735
|
+
|
|
736
|
+
**What it returns**
|
|
737
|
+
|
|
738
|
+
| Return value | Result |
|
|
739
|
+
| -------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
740
|
+
| a string or any JSON value | Sent to the agent (objects also as `structuredContent`). Still an error result if the route failed. |
|
|
741
|
+
| a full `ToolResult` | Used as is, so you control `isError` (e.g. `{ ...ctx.defaultResult(), content: [...] }`). |
|
|
742
|
+
| `undefined` | The default result. |
|
|
743
|
+
| a thrown error | An error result with the message, which the agent sees. |
|
|
744
|
+
|
|
745
|
+
`maxResponseChars` still applies to whatever the hook returns.
|
|
746
|
+
|
|
747
|
+
**For every tool at once**, set `transformResponse` in the server options, e.g. to strip fields everywhere:
|
|
748
|
+
|
|
749
|
+
```ts
|
|
750
|
+
mountMcp(app, {
|
|
751
|
+
name: 'shop-api',
|
|
752
|
+
transformResponse: (res) => (res.ok ? omitDeep(res.json, ['internalNotes', 'passwordHash']) : undefined),
|
|
753
|
+
});
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
A tool's own `transformResponse` replaces the server-level one.
|
|
757
|
+
|
|
651
758
|
## Configuration reference
|
|
652
759
|
|
|
653
760
|
### Server options (all adapters)
|
|
654
761
|
|
|
655
|
-
| Option
|
|
656
|
-
|
|
|
657
|
-
| `name`
|
|
658
|
-
| `version`
|
|
659
|
-
| `instructions`
|
|
660
|
-
| `path`
|
|
661
|
-
| `allowedOrigins`
|
|
662
|
-
| `forwardHeaders`
|
|
663
|
-
| `maxResponseChars`
|
|
664
|
-
| `oauth`
|
|
665
|
-
| `
|
|
666
|
-
| `
|
|
667
|
-
| `
|
|
668
|
-
| `
|
|
669
|
-
| `
|
|
670
|
-
| `
|
|
671
|
-
| `
|
|
672
|
-
| `
|
|
673
|
-
| `
|
|
674
|
-
| `
|
|
762
|
+
| Option | Type | Default | Description |
|
|
763
|
+
| ------------------- | --------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
764
|
+
| `name` | `string` | required | Server name shown to clients. |
|
|
765
|
+
| `version` | `string` | `'1.0.0'` | Server version. |
|
|
766
|
+
| `instructions` | `string` | none | Guidance for the model on how to use the tools together. |
|
|
767
|
+
| `path` | `string` | `'/mcp'` | Endpoint path. |
|
|
768
|
+
| `allowedOrigins` | `string[] \| '*'` | `[]` | Browser origins allowed to call the endpoint. Requests without `Origin` (CLIs, IDEs, servers) are always allowed. |
|
|
769
|
+
| `forwardHeaders` | `string[]` | `['authorization','cookie','x-api-key','accept-language']` | Headers copied from the MCP request to the internal API call. |
|
|
770
|
+
| `maxResponseChars` | `number` | `100000` | Longer API responses are truncated before reaching the model. |
|
|
771
|
+
| `oauth` | `OAuthOptions` | none | Protect the endpoint with OAuth 2.1. See [OAuth](#oauth-sign-in-from-ai-clients). |
|
|
772
|
+
| `transformResponse` | `ResponseTransform` | none | Default response hook for route tools without their own. See [Shaping responses](#shaping-and-enriching-responses). |
|
|
773
|
+
| `tools` | `McpToolDefinition[]` | `[]` | Extra hand-written tools. |
|
|
774
|
+
| `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. |
|
|
775
|
+
| `routes` | `{method,path,...}[]` | `[]` | _Express/Koa/Hono._ Expose routes without editing them. |
|
|
776
|
+
| `routers` | see guide | none | _Express:_ `{ '/prefix': router }`. _Koa:_ `[router]`. |
|
|
777
|
+
| `middleware` | `Middleware[]` | `[]` | _Express._ Middleware in front of `/mcp`, such as auth. |
|
|
778
|
+
| `guards` | `CanActivate[]` | `[]` | _NestJS._ Guards on the MCP controller. |
|
|
779
|
+
| `decorators` | `ClassDecorator[]` | `[]` | _NestJS._ Extra decorators on the MCP controller, e.g. `[Public()]` for global guards. |
|
|
780
|
+
| `pathPrefix` | `string` | none | _NestJS._ Extra prefix for tool routes. Global prefix and URI versioning are automatic. |
|
|
781
|
+
| `routeOptions` | `object` | none | _Fastify._ Extra route options for `/mcp`, such as `onRequest` hooks. |
|
|
782
|
+
| `configureRoute` | `(route) => void` | none | _AdonisJS._ Configure the MCP route, e.g. add middleware. |
|
|
675
783
|
|
|
676
784
|
### Tool options (`@McpTool()`, `mcpTool()`, `config.mcp`, `.mcp()`)
|
|
677
785
|
|
|
@@ -684,6 +792,7 @@ Your app's own validation always runs as well. The schema tells the agent what t
|
|
|
684
792
|
| `annotations` | MCP hints: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`. Defaults come from the HTTP method (GET → read-only, DELETE → destructive). |
|
|
685
793
|
| `headers` | Static headers added to the internal request. |
|
|
686
794
|
| `scopes` | OAuth scopes the token needs to call this tool (with the `oauth` option). Missing scopes answer `403 insufficient_scope`. |
|
|
795
|
+
| `transformResponse` | Shape or enrich what the agent sees from this route's response. See [Shaping and enriching responses](#shaping-and-enriching-responses). |
|
|
687
796
|
|
|
688
797
|
Each internal request also carries `X-Mcp-Tool: <tool name>`, so you can log or meter agent traffic separately.
|
|
689
798
|
|
|
@@ -740,7 +849,7 @@ Planned (non-breaking, 1.x minor releases):
|
|
|
740
849
|
|
|
741
850
|
- Per-user tool lists (hide tools the token's scopes can't call)
|
|
742
851
|
- Next.js route handlers, Hapi and Elysia adapters
|
|
743
|
-
- Structured output schemas, and binary/file responses
|
|
852
|
+
- Structured output schemas (`outputSchema`), and binary/file responses
|
|
744
853
|
- Streaming long-running responses over SSE
|
|
745
854
|
- CLI to preview generated tools (`npx mcp-expose inspect`)
|
|
746
855
|
- Resources from GET routes, and prompts
|
package/dist/adonisjs/index.cjs
CHANGED
|
@@ -98,6 +98,38 @@ function loopbackBaseUrl(req) {
|
|
|
98
98
|
}
|
|
99
99
|
__name(loopbackBaseUrl, "loopbackBaseUrl");
|
|
100
100
|
|
|
101
|
+
// src/core/result.ts
|
|
102
|
+
function isToolResult(value) {
|
|
103
|
+
const content = value?.content;
|
|
104
|
+
return !!value && typeof value === "object" && Array.isArray(content) && content.length > 0 && content.every((c) => !!c && typeof c === "object" && typeof c.type === "string");
|
|
105
|
+
}
|
|
106
|
+
__name(isToolResult, "isToolResult");
|
|
107
|
+
function toToolResult(value) {
|
|
108
|
+
if (isToolResult(value)) return value;
|
|
109
|
+
if (typeof value === "string") return {
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: "text",
|
|
113
|
+
text: value
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
};
|
|
117
|
+
const text = JSON.stringify(value ?? null);
|
|
118
|
+
const result = {
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: "text",
|
|
122
|
+
text
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
};
|
|
126
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
127
|
+
result.structuredContent = value;
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
__name(toToolResult, "toToolResult");
|
|
132
|
+
|
|
101
133
|
// src/core/schema.ts
|
|
102
134
|
function isStandard(s) {
|
|
103
135
|
return typeof s === "object" && s !== null && "~standard" in s;
|
|
@@ -318,9 +350,7 @@ function splitArgs(args, mapping) {
|
|
|
318
350
|
}
|
|
319
351
|
__name(splitArgs, "splitArgs");
|
|
320
352
|
function responseToResult(res, maxChars) {
|
|
321
|
-
|
|
322
|
-
if (text.length > maxChars) text = `${text.slice(0, maxChars)}
|
|
323
|
-
\u2026[truncated ${text.length - maxChars} chars]`;
|
|
353
|
+
const text = truncate(res.body, maxChars);
|
|
324
354
|
const isError = res.status >= 400;
|
|
325
355
|
const result = {
|
|
326
356
|
content: [
|
|
@@ -345,6 +375,58 @@ ${text}` : text || `HTTP ${res.status}`
|
|
|
345
375
|
return result;
|
|
346
376
|
}
|
|
347
377
|
__name(responseToResult, "responseToResult");
|
|
378
|
+
function safeDecode(segment) {
|
|
379
|
+
try {
|
|
380
|
+
return decodeURIComponent(segment);
|
|
381
|
+
} catch {
|
|
382
|
+
return segment;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
__name(safeDecode, "safeDecode");
|
|
386
|
+
function truncate(text, maxChars) {
|
|
387
|
+
return text.length > maxChars ? `${text.slice(0, maxChars)}
|
|
388
|
+
\u2026[truncated ${text.length - maxChars} chars]` : text;
|
|
389
|
+
}
|
|
390
|
+
__name(truncate, "truncate");
|
|
391
|
+
function toRouteToolResponse(res) {
|
|
392
|
+
let json;
|
|
393
|
+
if ((res.headers["content-type"] ?? "").includes("json") && res.body) {
|
|
394
|
+
try {
|
|
395
|
+
json = JSON.parse(res.body);
|
|
396
|
+
} catch {
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
...res,
|
|
401
|
+
json,
|
|
402
|
+
ok: res.status < 400
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
__name(toRouteToolResponse, "toRouteToolResponse");
|
|
406
|
+
function transformedResult(value, res, maxChars) {
|
|
407
|
+
const explicit = isToolResult(value);
|
|
408
|
+
const result = toToolResult(value);
|
|
409
|
+
if (!explicit && !res.ok) {
|
|
410
|
+
result.isError = true;
|
|
411
|
+
delete result.structuredContent;
|
|
412
|
+
}
|
|
413
|
+
let truncated = false;
|
|
414
|
+
const content = result.content.map((c) => {
|
|
415
|
+
if (c.type !== "text" || c.text.length <= maxChars) return c;
|
|
416
|
+
truncated = true;
|
|
417
|
+
return {
|
|
418
|
+
...c,
|
|
419
|
+
text: truncate(c.text, maxChars)
|
|
420
|
+
};
|
|
421
|
+
});
|
|
422
|
+
if (!truncated) return result;
|
|
423
|
+
const { structuredContent: _dropped, ...rest } = result;
|
|
424
|
+
return {
|
|
425
|
+
...rest,
|
|
426
|
+
content
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
__name(transformedResult, "transformedResult");
|
|
348
430
|
function pickForwardHeaders(ctx, names) {
|
|
349
431
|
const out = {};
|
|
350
432
|
for (const name of names) {
|
|
@@ -360,6 +442,7 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
|
|
|
360
442
|
const { schema, mapping } = buildInputSchema(route, opts);
|
|
361
443
|
const forward = serverOpts.forwardHeaders ?? DEFAULT_FORWARD_HEADERS;
|
|
362
444
|
const maxChars = serverOpts.maxResponseChars ?? 1e5;
|
|
445
|
+
const transform = opts.transformResponse ?? serverOpts.transformResponse;
|
|
363
446
|
return {
|
|
364
447
|
name,
|
|
365
448
|
title: opts.title,
|
|
@@ -405,14 +488,53 @@ function createRouteTool(route, opts, dispatch, serverOpts = {}) {
|
|
|
405
488
|
]
|
|
406
489
|
};
|
|
407
490
|
}
|
|
408
|
-
const
|
|
491
|
+
const request = {
|
|
409
492
|
method,
|
|
410
493
|
path,
|
|
411
494
|
query,
|
|
412
495
|
body,
|
|
413
496
|
headers
|
|
414
|
-
}
|
|
415
|
-
|
|
497
|
+
};
|
|
498
|
+
const res = await dispatch(request, ctx);
|
|
499
|
+
if (!transform) return responseToResult(res, maxChars);
|
|
500
|
+
const { "content-type": _ct, ...baseHeaders } = headers;
|
|
501
|
+
const hookCtx = {
|
|
502
|
+
tool: name,
|
|
503
|
+
args,
|
|
504
|
+
request,
|
|
505
|
+
toolContext: ctx,
|
|
506
|
+
defaultResult: /* @__PURE__ */ __name(() => responseToResult(res, maxChars), "defaultResult"),
|
|
507
|
+
async callRoute(r) {
|
|
508
|
+
if (!r.path.startsWith("/")) {
|
|
509
|
+
throw new Error(`callRoute: path must start with "/", got ${JSON.stringify(r.path)}`);
|
|
510
|
+
}
|
|
511
|
+
const params = r.params ?? {};
|
|
512
|
+
const missing2 = pathParams(r.path).filter((p) => params[p] === void 0 && !r.path.includes(`:${p}?`));
|
|
513
|
+
if (missing2.length) throw new Error(`callRoute: missing params for ${r.path}: ${missing2.join(", ")}`);
|
|
514
|
+
const subPath = interpolatePath(r.path, params);
|
|
515
|
+
if (subPath.split("/").some((seg) => [
|
|
516
|
+
".",
|
|
517
|
+
".."
|
|
518
|
+
].includes(safeDecode(seg)))) {
|
|
519
|
+
throw new Error(`callRoute: path must not contain "." or ".." segments: ${JSON.stringify(subPath)}`);
|
|
520
|
+
}
|
|
521
|
+
const sub = {
|
|
522
|
+
method: (r.method ?? "GET").toUpperCase(),
|
|
523
|
+
path: subPath,
|
|
524
|
+
query: r.query ?? {},
|
|
525
|
+
body: r.body,
|
|
526
|
+
headers: {
|
|
527
|
+
...baseHeaders,
|
|
528
|
+
...r.headers
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
if (sub.body !== void 0) sub.headers["content-type"] = "application/json";
|
|
532
|
+
return toRouteToolResponse(await dispatch(sub, ctx));
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
const response = toRouteToolResponse(res);
|
|
536
|
+
const value = await transform(response, hookCtx);
|
|
537
|
+
return value === void 0 ? responseToResult(res, maxChars) : transformedResult(value, response, maxChars);
|
|
416
538
|
}
|
|
417
539
|
};
|
|
418
540
|
}
|