mcp-expose 1.0.0 → 1.1.1
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 +36 -1
- package/README.md +181 -32
- package/dist/adonisjs/index.cjs +366 -6
- package/dist/adonisjs/index.cjs.map +1 -1
- package/dist/adonisjs/index.d.cts +2 -1
- package/dist/adonisjs/index.d.ts +2 -1
- package/dist/adonisjs/index.js +366 -6
- package/dist/adonisjs/index.js.map +1 -1
- package/dist/express/index.cjs +364 -7
- package/dist/express/index.cjs.map +1 -1
- package/dist/express/index.d.cts +4 -2
- package/dist/express/index.d.ts +4 -2
- package/dist/express/index.js +364 -7
- package/dist/express/index.js.map +1 -1
- package/dist/fastify/index.cjs +373 -6
- package/dist/fastify/index.cjs.map +1 -1
- package/dist/fastify/index.d.cts +2 -1
- package/dist/fastify/index.d.ts +2 -1
- package/dist/fastify/index.js +373 -6
- package/dist/fastify/index.js.map +1 -1
- package/dist/hono/index.cjs +367 -6
- package/dist/hono/index.cjs.map +1 -1
- package/dist/hono/index.d.cts +2 -1
- package/dist/hono/index.d.ts +2 -1
- package/dist/hono/index.js +367 -6
- package/dist/hono/index.js.map +1 -1
- package/dist/index.cjs +371 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.js +367 -31
- package/dist/index.js.map +1 -1
- package/dist/koa/index.cjs +366 -6
- package/dist/koa/index.cjs.map +1 -1
- package/dist/koa/index.d.cts +2 -1
- package/dist/koa/index.d.ts +2 -1
- package/dist/koa/index.js +366 -6
- package/dist/koa/index.js.map +1 -1
- package/dist/nestjs/index.cjs +381 -7
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.d.cts +21 -7
- package/dist/nestjs/index.d.ts +21 -7
- package/dist/nestjs/index.js +382 -8
- package/dist/nestjs/index.js.map +1 -1
- package/dist/oauth/index.cjs +214 -0
- package/dist/oauth/index.cjs.map +1 -0
- package/dist/oauth/index.d.cts +69 -0
- package/dist/oauth/index.d.ts +69 -0
- package/dist/oauth/index.js +177 -0
- package/dist/oauth/index.js.map +1 -0
- package/dist/oauth-8RmQamGP.d.cts +75 -0
- package/dist/oauth-8RmQamGP.d.ts +75 -0
- package/dist/{server-fzkb-wJo.d.ts → server--mQVQq3A.d.cts} +81 -1
- package/dist/{server-fzkb-wJo.d.cts → server-BPDTsT2o.d.ts} +81 -1
- package/package.json +22 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,39 @@ 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.1] - 2026-09-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `transformResponse` hook for route tools, on every marker and adapter, plus a server-level default. It shapes what
|
|
14
|
+
the agent sees from a route's response (hide or format fields, add hints) without turning the route into a custom
|
|
15
|
+
tool. The hook receives the status, headers, body and parsed JSON, the tool arguments and the MCP call context.
|
|
16
|
+
`ctx.callRoute()` enriches the response with other routes of the app, using the same credentials and the same
|
|
17
|
+
middleware, guards and validation. Placeholders are URL-encoded, and `.` / `..` segments are rejected.
|
|
18
|
+
- `ResponseTransform`, `ResponseTransformContext` and `RouteToolResponse` types.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- A tool handler or hook that returns plain data with its own `content` array (for example a CMS post) is no longer
|
|
23
|
+
mistaken for an MCP `ToolResult`; only `content` arrays of MCP content items (`{ type: ... }`) are.
|
|
24
|
+
|
|
25
|
+
## [1.1.0] - 2026-09-27
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **OAuth 2.1 support** following the MCP authorization spec, on every adapter, enabled with the new `oauth` server option:
|
|
30
|
+
- Protected Resource Metadata (RFC 9728) served at `/.well-known/oauth-protected-resource[/<path>]`, public and CORS-enabled.
|
|
31
|
+
- `401` / `403` responses with `WWW-Authenticate` challenges (`resource_metadata`, `scope`, `invalid_token`,
|
|
32
|
+
`insufficient_scope`), so MCP clients discover the authorization server and sign the user in by themselves.
|
|
33
|
+
- Audience-checked token verification with `requiredScopes`; the verified token is available to tools as `ctx.auth`.
|
|
34
|
+
- Per-tool `scopes` with `403 insufficient_scope` step-up.
|
|
35
|
+
- New `mcp-expose/oauth` entry point: `jwtVerifier()` (JWKS, via the optional `jose` peer dependency),
|
|
36
|
+
`introspectionVerifier()` (RFC 7662) and `discoverAuthorizationServer()`.
|
|
37
|
+
- NestJS: `decorators` option to add decorators (such as `Public()`) to the MCP controller.
|
|
38
|
+
- E2E: every scenario (each supported framework major) also runs in OAuth mode with `jwtVerifier`, and a new
|
|
39
|
+
scenario has the official MCP SDK client register dynamically, sign in with authorization code + PKCE and a
|
|
40
|
+
resource indicator, and step up scopes.
|
|
41
|
+
|
|
9
42
|
## [1.0.0] - 2026-09-25
|
|
10
43
|
|
|
11
44
|
First stable release. The public API (`mcp-expose` and `mcp-expose/<framework>` exports, tool and server
|
|
@@ -35,5 +68,7 @@ options, default tool naming) now follows Semantic Versioning.
|
|
|
35
68
|
- Path parameters that are empty, `.` or `..` are rejected, so an agent cannot use them to reach routes
|
|
36
69
|
that were never exposed as tools (for example `/orders/../admin`).
|
|
37
70
|
|
|
38
|
-
[Unreleased]: https://github.com/NITINKACHHADIYA/Node-MCP/compare/v1.
|
|
71
|
+
[Unreleased]: https://github.com/NITINKACHHADIYA/Node-MCP/compare/v1.1.1...HEAD
|
|
72
|
+
[1.1.1]: https://github.com/NITINKACHHADIYA/Node-MCP/releases/tag/v1.1.1
|
|
73
|
+
[1.1.0]: https://github.com/NITINKACHHADIYA/Node-MCP/releases/tag/v1.1.0
|
|
39
74
|
[1.0.0]: https://github.com/NITINKACHHADIYA/Node-MCP/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@ router.get('/orders/:id', [OrdersController, 'show']).use(middleware.auth()).mcp
|
|
|
32
32
|
- Zero runtime dependencies. Dual ESM/CJS. Node.js 20, 22 and 24, plus Bun, Deno and Workers for Hono.
|
|
33
33
|
- Supports the current **and the two previous major versions** of every framework, verified end to end.
|
|
34
34
|
- Speaks MCP Streamable HTTP (protocol `2024-11-05` → `2025-11-25`), stateless, so it scales horizontally and runs serverless.
|
|
35
|
+
- Built-in [OAuth 2.1](#oauth-sign-in-from-ai-clients): AI clients discover your identity provider and sign the user in by themselves (Auth0, Okta, Keycloak, Entra ID, Clerk, Cognito, ...).
|
|
35
36
|
|
|
36
37
|
---
|
|
37
38
|
|
|
@@ -44,14 +45,16 @@ router.get('/orders/:id', [OrdersController, 'show']).use(middleware.auth()).mcp
|
|
|
44
45
|
5. [Framework guides](#framework-guides)
|
|
45
46
|
- [NestJS](#nestjs) · [Express](#express) · [Fastify](#fastify) · [Koa](#koa) · [Hono](#hono) · [AdonisJS](#adonisjs) · [Any API via OpenAPI](#any-api-via-openapi-standalone-gateway)
|
|
46
47
|
6. [Connect an AI client](#connect-an-ai-client)
|
|
47
|
-
7. [
|
|
48
|
-
8. [
|
|
49
|
-
9. [
|
|
50
|
-
10. [
|
|
51
|
-
11. [
|
|
52
|
-
12. [
|
|
53
|
-
13. [
|
|
54
|
-
14. [
|
|
48
|
+
7. [OAuth: sign in from AI clients](#oauth-sign-in-from-ai-clients)
|
|
49
|
+
8. [Defining tool inputs (schemas)](#defining-tool-inputs-schemas)
|
|
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)
|
|
55
58
|
|
|
56
59
|
---
|
|
57
60
|
|
|
@@ -466,7 +469,8 @@ This also works with `@nestjs/swagger`, `@fastify/swagger`, tsoa and hono-openap
|
|
|
466
469
|
## Connect an AI client
|
|
467
470
|
|
|
468
471
|
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.
|
|
472
|
+
client would use. They are forwarded to your routes. (With the [`oauth` option](#oauth-sign-in-from-ai-clients)
|
|
473
|
+
you skip the header: the client signs the user in by itself.)
|
|
470
474
|
|
|
471
475
|
**Claude Code**
|
|
472
476
|
|
|
@@ -534,6 +538,89 @@ curl -s localhost:3000/mcp -H 'content-type: application/json' \
|
|
|
534
538
|
|
|
535
539
|
---
|
|
536
540
|
|
|
541
|
+
## OAuth: sign in from AI clients
|
|
542
|
+
|
|
543
|
+
Pasting a token into a client config works for development. For real users, turn on the `oauth` option:
|
|
544
|
+
mcp-expose then implements the [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization),
|
|
545
|
+
so Claude, Cursor, VS Code and other clients open your normal login page, get a token for the user, and
|
|
546
|
+
send it on every call. Nothing is pasted by hand.
|
|
547
|
+
|
|
548
|
+
```
|
|
549
|
+
AI client ── POST /mcp (no token) ─────────────▶ 401 WWW-Authenticate: Bearer resource_metadata=".../.well-known/oauth-protected-resource/mcp"
|
|
550
|
+
AI client ── GET /.well-known/oauth-protected-resource/mcp ─▶ { resource, authorization_servers: ["https://login.example.com"] }
|
|
551
|
+
AI client ◀──── login in the browser (your IdP: Auth0, Okta, Keycloak, Entra ID, Clerk, Cognito, ...) ────▶ access token
|
|
552
|
+
AI client ── POST /mcp Authorization: Bearer <token> ─▶ mcp-expose checks the token ─▶ your route (token forwarded, guards run)
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
mcp-expose is the **resource server**. Your identity provider stays the **authorization server**, so user
|
|
556
|
+
accounts, login pages and MFA stay where they are.
|
|
557
|
+
|
|
558
|
+
### 1. Configure it
|
|
559
|
+
|
|
560
|
+
```ts
|
|
561
|
+
import { jwtVerifier } from 'mcp-expose/oauth'; // npm install jose
|
|
562
|
+
|
|
563
|
+
const oauth = {
|
|
564
|
+
// The public URL of your MCP endpoint. Tokens must be issued for exactly this audience.
|
|
565
|
+
resource: 'https://api.example.com/mcp',
|
|
566
|
+
authorizationServers: ['https://example.auth0.com/'],
|
|
567
|
+
requiredScopes: ['mcp:tools'], // optional: needed for any MCP access
|
|
568
|
+
verifyToken: jwtVerifier({ issuer: 'https://example.auth0.com/' }),
|
|
569
|
+
};
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Pass it as `oauth` to any adapter:
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
McpModule.forRoot({ name: 'shop-api', oauth, decorators: [Public()] }); // NestJS (see note below)
|
|
576
|
+
mountMcp(app, { name: 'shop-api', oauth }); // Express, Koa, Hono, AdonisJS
|
|
577
|
+
await app.register(fastifyMcp, { name: 'shop-api', oauth }); // Fastify
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
That's it. The adapter:
|
|
581
|
+
|
|
582
|
+
- serves the Protected Resource Metadata ([RFC 9728](https://www.rfc-editor.org/rfc/rfc9728)) at `/.well-known/oauth-protected-resource/mcp` (and `/.well-known/oauth-protected-resource`), public and CORS-enabled, outside any global prefix, guard or MCP middleware;
|
|
583
|
+
- answers requests without a valid token with `401` and a `WWW-Authenticate` challenge that starts the client's login;
|
|
584
|
+
- rejects tokens that were not issued for this resource (audience check), which blocks token passthrough from other services;
|
|
585
|
+
- forwards the verified `Authorization` header to your routes, so your guards see the same user;
|
|
586
|
+
- exposes the verified token to custom tools as `ctx.auth` (`subject`, `scopes`, `clientId`, `claims`).
|
|
587
|
+
|
|
588
|
+
### 2. Per-tool scopes (step-up)
|
|
589
|
+
|
|
590
|
+
```ts
|
|
591
|
+
@Delete(':id')
|
|
592
|
+
@McpTool({ name: 'cancel_order', scopes: ['orders:write'] })
|
|
593
|
+
cancel(@Param('id') id: string) { ... }
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
A token without `orders:write` gets `403 insufficient_scope` naming the scopes it needs (plus the ones it already has, so the new token doesn't lose access). MCP clients then ask
|
|
597
|
+
the user to approve the extra access and retry. All tool scopes are advertised in `scopes_supported`.
|
|
598
|
+
`scopes` works on every marker (`mcpTool()`, `config.mcp`, `.mcp()`, `defineTool()`).
|
|
599
|
+
|
|
600
|
+
### Token verifiers
|
|
601
|
+
|
|
602
|
+
| Verifier | Use it for |
|
|
603
|
+
| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
604
|
+
| `jwtVerifier({ issuer })` | JWT access tokens. Keys come from the issuer's JWKS (found through its metadata, or set `jwksUri` / `jwks`). Checks signature (asymmetric algorithms only), `iss`, `aud`, `exp`, `nbf`. Needs `jose`. |
|
|
605
|
+
| `introspectionVerifier({ issuer, clientId, clientSecret })` | Opaque tokens, via [RFC 7662](https://www.rfc-editor.org/rfc/rfc7662) introspection (Keycloak, Okta, ORY, Authlete, ...). Checks `active`, `aud` and `iss`, and caches results for 60 s. |
|
|
606
|
+
| `verifyToken: async (token, { resource }) => AuthInfo \| undefined` | Anything else, such as your existing session or API-key lookup. Return `undefined` to reject the token. |
|
|
607
|
+
|
|
608
|
+
`jwtVerifier` checks that `aud` equals `resource` by default. If your provider uses another audience
|
|
609
|
+
identifier (for example an Auth0 API identifier), set `audience`.
|
|
610
|
+
|
|
611
|
+
### Provider notes
|
|
612
|
+
|
|
613
|
+
- **The authorization server must support the MCP client flow:** authorization code with PKCE, and ideally
|
|
614
|
+
dynamic client registration ([RFC 7591](https://www.rfc-editor.org/rfc/rfc7591)) or client ID metadata documents,
|
|
615
|
+
so clients can register themselves. Many providers support this (for example Auth0, Keycloak, WorkOS and Stytch); with others, pre-register the client and give users its client ID.
|
|
616
|
+
- **Your routes must accept the same token.** It is forwarded unchanged. If your API normally checks a different
|
|
617
|
+
audience, allow the MCP resource too, or set `forwardHeaders` to drop `authorization` and authorise
|
|
618
|
+
with a custom tool using `ctx.auth`.
|
|
619
|
+
- **NestJS global guards** (an `APP_GUARD` that requires a session) run before the MCP controller and would answer 401 without the
|
|
620
|
+
OAuth challenge. Pass your "public" decorator with `decorators: [Public()]` so the guard lets MCP requests through;
|
|
621
|
+
mcp-expose checks the token itself. Likewise, don't also put auth `middleware`, `guards`, `routeOptions` hooks or `configureRoute` middleware on the MCP endpoint.
|
|
622
|
+
- **Behind a proxy**, `resource` must be the public URL the client connects to (`https://...`), not the internal one.
|
|
623
|
+
|
|
537
624
|
## Defining tool inputs (schemas)
|
|
538
625
|
|
|
539
626
|
The agent sees **one flat object** of arguments. mcp-expose maps each argument back to the right place:
|
|
@@ -562,28 +649,88 @@ Sources, from highest to lowest precedence:
|
|
|
562
649
|
|
|
563
650
|
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
651
|
|
|
652
|
+
## Shaping and enriching responses
|
|
653
|
+
|
|
654
|
+
By default the agent gets the route's response as it is. Add `transformResponse` to a route tool to change
|
|
655
|
+
what the agent sees: hide internal fields, format values, add hints, or pull in related data from other
|
|
656
|
+
routes. The route stays a route tool; there is no need to rewrite it as a custom tool.
|
|
657
|
+
|
|
658
|
+
```ts
|
|
659
|
+
@Get(':id')
|
|
660
|
+
@McpTool({
|
|
661
|
+
name: 'get_order',
|
|
662
|
+
transformResponse: async (res, ctx) => {
|
|
663
|
+
if (!res.ok) return undefined; // keep the default error result
|
|
664
|
+
const { internalNotes, ...order } = res.json as Order;
|
|
665
|
+
// Another route of this app, with the same user's credentials, guards and pipes:
|
|
666
|
+
const customer = await ctx.callRoute({ path: '/api/customers/:id', params: { id: order.customerId } });
|
|
667
|
+
return { ...order, total: formatMoney(order.total), customer: customer.ok ? customer.json : null };
|
|
668
|
+
},
|
|
669
|
+
})
|
|
670
|
+
findOne(@Param('id') id: string) { ... }
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
It works the same with every marker: `mcpTool({ transformResponse })`, `config: { mcp: { transformResponse } }`,
|
|
674
|
+
`.mcp({ transformResponse })`, and `toolsFromOpenApi(doc, dispatch, { transformResponse })`.
|
|
675
|
+
|
|
676
|
+
**What the hook receives**
|
|
677
|
+
|
|
678
|
+
- `res`: `status`, `headers`, the raw `body`, the parsed `json` (for JSON responses) and `ok` (status below 400).
|
|
679
|
+
- `ctx.args`: the tool arguments. `ctx.request`: the internal request (method, path, query, body).
|
|
680
|
+
- `ctx.toolContext`: the MCP request's headers, client IP and, with [OAuth](#oauth-sign-in-from-ai-clients), `auth`.
|
|
681
|
+
- `ctx.defaultResult()`: the result mcp-expose would return without the hook.
|
|
682
|
+
- `ctx.callRoute({ method, path, params, query, body, headers })`: calls another route of the same app.
|
|
683
|
+
- 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}`
|
|
685
|
+
placeholders from `params`: values are URL-encoded, and `.` or `..` are rejected, so agent input can't redirect the call.
|
|
686
|
+
|
|
687
|
+
**What it returns**
|
|
688
|
+
|
|
689
|
+
| Return value | Result |
|
|
690
|
+
| -------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
691
|
+
| a string or any JSON value | Sent to the agent (objects also as `structuredContent`). Still an error result if the route failed. |
|
|
692
|
+
| a full `ToolResult` | Used as is, so you control `isError` (e.g. `{ ...ctx.defaultResult(), content: [...] }`). |
|
|
693
|
+
| `undefined` | The default result. |
|
|
694
|
+
| a thrown error | An error result with the message, which the agent sees. |
|
|
695
|
+
|
|
696
|
+
`maxResponseChars` still applies to whatever the hook returns.
|
|
697
|
+
|
|
698
|
+
**For every tool at once**, set `transformResponse` in the server options, e.g. to strip fields everywhere:
|
|
699
|
+
|
|
700
|
+
```ts
|
|
701
|
+
mountMcp(app, {
|
|
702
|
+
name: 'shop-api',
|
|
703
|
+
transformResponse: (res) => (res.ok ? omitDeep(res.json, ['internalNotes', 'passwordHash']) : undefined),
|
|
704
|
+
});
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
A tool's own `transformResponse` replaces the server-level one.
|
|
708
|
+
|
|
565
709
|
## Configuration reference
|
|
566
710
|
|
|
567
711
|
### Server options (all adapters)
|
|
568
712
|
|
|
569
|
-
| Option
|
|
570
|
-
|
|
|
571
|
-
| `name`
|
|
572
|
-
| `version`
|
|
573
|
-
| `instructions`
|
|
574
|
-
| `path`
|
|
575
|
-
| `allowedOrigins`
|
|
576
|
-
| `forwardHeaders`
|
|
577
|
-
| `maxResponseChars`
|
|
578
|
-
| `
|
|
579
|
-
| `
|
|
580
|
-
| `
|
|
581
|
-
| `
|
|
582
|
-
| `
|
|
583
|
-
| `
|
|
584
|
-
| `
|
|
585
|
-
| `
|
|
586
|
-
| `
|
|
713
|
+
| Option | Type | Default | Description |
|
|
714
|
+
| ------------------- | --------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
715
|
+
| `name` | `string` | required | Server name shown to clients. |
|
|
716
|
+
| `version` | `string` | `'1.0.0'` | Server version. |
|
|
717
|
+
| `instructions` | `string` | none | Guidance for the model on how to use the tools together. |
|
|
718
|
+
| `path` | `string` | `'/mcp'` | Endpoint path. |
|
|
719
|
+
| `allowedOrigins` | `string[] \| '*'` | `[]` | Browser origins allowed to call the endpoint. Requests without `Origin` (CLIs, IDEs, servers) are always allowed. |
|
|
720
|
+
| `forwardHeaders` | `string[]` | `['authorization','cookie','x-api-key','accept-language']` | Headers copied from the MCP request to the internal API call. |
|
|
721
|
+
| `maxResponseChars` | `number` | `100000` | Longer API responses are truncated before reaching the model. |
|
|
722
|
+
| `oauth` | `OAuthOptions` | none | Protect the endpoint with OAuth 2.1. See [OAuth](#oauth-sign-in-from-ai-clients). |
|
|
723
|
+
| `transformResponse` | `ResponseTransform` | none | Default response hook for route tools without their own. See [Shaping responses](#shaping-and-enriching-responses). |
|
|
724
|
+
| `tools` | `McpToolDefinition[]` | `[]` | Extra hand-written tools. |
|
|
725
|
+
| `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. |
|
|
726
|
+
| `routes` | `{method,path,...}[]` | `[]` | _Express/Koa/Hono._ Expose routes without editing them. |
|
|
727
|
+
| `routers` | see guide | none | _Express:_ `{ '/prefix': router }`. _Koa:_ `[router]`. |
|
|
728
|
+
| `middleware` | `Middleware[]` | `[]` | _Express._ Middleware in front of `/mcp`, such as auth. |
|
|
729
|
+
| `guards` | `CanActivate[]` | `[]` | _NestJS._ Guards on the MCP controller. |
|
|
730
|
+
| `decorators` | `ClassDecorator[]` | `[]` | _NestJS._ Extra decorators on the MCP controller, e.g. `[Public()]` for global guards. |
|
|
731
|
+
| `pathPrefix` | `string` | none | _NestJS._ Extra prefix for tool routes. Global prefix and URI versioning are automatic. |
|
|
732
|
+
| `routeOptions` | `object` | none | _Fastify._ Extra route options for `/mcp`, such as `onRequest` hooks. |
|
|
733
|
+
| `configureRoute` | `(route) => void` | none | _AdonisJS._ Configure the MCP route, e.g. add middleware. |
|
|
587
734
|
|
|
588
735
|
### Tool options (`@McpTool()`, `mcpTool()`, `config.mcp`, `.mcp()`)
|
|
589
736
|
|
|
@@ -595,6 +742,8 @@ Your app's own validation always runs as well. The schema tells the agent what t
|
|
|
595
742
|
| `input` / `params` / `query` / `body` | Schemas, see [above](#defining-tool-inputs-schemas). |
|
|
596
743
|
| `annotations` | MCP hints: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`. Defaults come from the HTTP method (GET → read-only, DELETE → destructive). |
|
|
597
744
|
| `headers` | Static headers added to the internal request. |
|
|
745
|
+
| `scopes` | OAuth scopes the token needs to call this tool (with the `oauth` option). Missing scopes answer `403 insufficient_scope`. |
|
|
746
|
+
| `transformResponse` | Shape or enrich what the agent sees from this route's response. See [Shaping and enriching responses](#shaping-and-enriching-responses). |
|
|
598
747
|
|
|
599
748
|
Each internal request also carries `X-Mcp-Tool: <tool name>`, so you can log or meter agent traffic separately.
|
|
600
749
|
|
|
@@ -616,12 +765,12 @@ const convert = defineTool({
|
|
|
616
765
|
mountMcp(app, { name: 'shop-api', tools: [convert] });
|
|
617
766
|
```
|
|
618
767
|
|
|
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.
|
|
768
|
+
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. With the `oauth` option, `ctx.auth` holds the verified token (`subject`, `scopes`, `clientId`, `claims`).
|
|
620
769
|
|
|
621
770
|
## Security checklist
|
|
622
771
|
|
|
623
772
|
- **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.
|
|
773
|
+
- **Use per-user credentials.** Clients send their own token, the token is forwarded, and your guards authorise the call. Avoid one shared super-token. For remote servers, prefer the [`oauth` option](#oauth-sign-in-from-ai-clients): users sign in through your identity provider, tokens are audience-checked, and sensitive tools can require extra `scopes`.
|
|
625
774
|
- **Protect discovery too** if tool names are sensitive (`guards`, `middleware`, `routeOptions`).
|
|
626
775
|
- **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
776
|
- **Browser access:** keep `allowedOrigins` empty unless a browser app must call `/mcp` directly.
|
|
@@ -649,9 +798,9 @@ Current scope (1.x):
|
|
|
649
798
|
|
|
650
799
|
Planned (non-breaking, 1.x minor releases):
|
|
651
800
|
|
|
652
|
-
-
|
|
801
|
+
- Per-user tool lists (hide tools the token's scopes can't call)
|
|
653
802
|
- Next.js route handlers, Hapi and Elysia adapters
|
|
654
|
-
- Structured output schemas, and binary/file responses
|
|
803
|
+
- Structured output schemas (`outputSchema`), and binary/file responses
|
|
655
804
|
- Streaming long-running responses over SSE
|
|
656
805
|
- CLI to preview generated tools (`npx mcp-expose inspect`)
|
|
657
806
|
- Resources from GET routes, and prompts
|
|
@@ -663,7 +812,7 @@ Contributions are welcome. See [Development](#development).
|
|
|
663
812
|
```bash
|
|
664
813
|
npm install
|
|
665
814
|
npm test # vitest: core + all six adapters (real servers, real HTTP)
|
|
666
|
-
npm run test:e2e # pack → install into
|
|
815
|
+
npm run test:e2e # pack → install into 19 fresh framework projects → official MCP SDK client (with and without OAuth)
|
|
667
816
|
npm run typecheck
|
|
668
817
|
npm run build # ESM + CJS + .d.ts into dist/
|
|
669
818
|
|