@ttoss/http-server-mcp 0.12.5 → 0.13.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/README.md +167 -17
- package/dist/index.cjs +86 -4
- package/dist/index.d.cts +106 -1
- package/dist/index.d.mts +106 -1
- package/dist/index.mjs +85 -5
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -127,6 +127,137 @@ const data = await apiCall('GET', 'https://partner.api.com/data', {
|
|
|
127
127
|
|
|
128
128
|
`apiCall` throws with a clear message when called with a relative path and no `apiBaseUrl` is configured in the context.
|
|
129
129
|
|
|
130
|
+
## Authentication
|
|
131
|
+
|
|
132
|
+
`createMcpRouter` supports OAuth 2.0 Bearer token authentication via the `auth` option. Every incoming MCP request must include a valid `Authorization: Bearer <token>` header — invalid or missing tokens receive a `401 Unauthorized` response.
|
|
133
|
+
|
|
134
|
+
```mermaid
|
|
135
|
+
sequenceDiagram
|
|
136
|
+
participant Client
|
|
137
|
+
participant MCP Server
|
|
138
|
+
participant Verifier
|
|
139
|
+
|
|
140
|
+
Client->>MCP Server: POST /mcp + Authorization: Bearer <token>
|
|
141
|
+
MCP Server->>Verifier: verify(token)
|
|
142
|
+
alt valid token
|
|
143
|
+
Verifier-->>MCP Server: identity payload
|
|
144
|
+
MCP Server->>MCP Server: run tool (identity available via getIdentity())
|
|
145
|
+
MCP Server-->>Client: 200 OK
|
|
146
|
+
else invalid or missing token
|
|
147
|
+
Verifier-->>MCP Server: error
|
|
148
|
+
MCP Server-->>Client: 401 Unauthorized
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Amazon Cognito
|
|
153
|
+
|
|
154
|
+
Pass `cognitoUserPool` and the router creates a `CognitoJwtVerifier` (from `@ttoss/auth-core`) internally:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { createMcpRouter, McpServer } from '@ttoss/http-server-mcp';
|
|
158
|
+
|
|
159
|
+
const mcpRouter = createMcpRouter(mcpServer, {
|
|
160
|
+
auth: {
|
|
161
|
+
cognitoUserPool: {
|
|
162
|
+
userPoolId: process.env.COGNITO_USER_POOL_ID!,
|
|
163
|
+
clientId: process.env.COGNITO_CLIENT_ID!,
|
|
164
|
+
tokenUse: 'access', // default
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Custom verifier
|
|
171
|
+
|
|
172
|
+
Pass an async `verifyToken` function for any other provider (Auth0, Keycloak, etc.):
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { createMcpRouter } from '@ttoss/http-server-mcp';
|
|
176
|
+
import { jwtVerify, createRemoteJWKSet } from 'jose';
|
|
177
|
+
|
|
178
|
+
const JWKS = createRemoteJWKSet(
|
|
179
|
+
new URL('https://your-auth-server/.well-known/jwks.json')
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const mcpRouter = createMcpRouter(mcpServer, {
|
|
183
|
+
auth: {
|
|
184
|
+
verifyToken: async (token) => {
|
|
185
|
+
const { payload } = await jwtVerify(token, JWKS);
|
|
186
|
+
return payload;
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Accessing the verified identity
|
|
193
|
+
|
|
194
|
+
Inside any tool handler, call `getIdentity()` to retrieve the verified JWT payload:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { getIdentity, createMcpRouter, McpServer } from '@ttoss/http-server-mcp';
|
|
198
|
+
|
|
199
|
+
mcpServer.registerTool(
|
|
200
|
+
'get-profile',
|
|
201
|
+
{ description: 'Return the caller's profile', inputSchema: {} },
|
|
202
|
+
async () => {
|
|
203
|
+
const identity = getIdentity() as { sub: string; email: string };
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: 'text', text: `Hello, ${identity.email}` }],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Scope enforcement
|
|
212
|
+
|
|
213
|
+
Scopes can be enforced at two levels.
|
|
214
|
+
|
|
215
|
+
**Router-level** — gate the entire MCP endpoint. Any token missing a required scope receives a `403 Forbidden` before any tool runs:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
createMcpRouter(mcpServer, {
|
|
219
|
+
auth: {
|
|
220
|
+
cognitoUserPool: { userPoolId: '...', clientId: '...' },
|
|
221
|
+
requiredScopes: ['mcp:access'],
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Per-tool** — use `checkScopes()` inside individual handlers for fine-grained control. It throws an error that the MCP SDK returns as a tool error to the client:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { checkScopes, getIdentity } from '@ttoss/http-server-mcp';
|
|
230
|
+
|
|
231
|
+
mcpServer.registerTool(
|
|
232
|
+
'delete-user',
|
|
233
|
+
{ description: 'Delete a user', inputSchema: { userId: z.string() } },
|
|
234
|
+
async ({ userId }) => {
|
|
235
|
+
checkScopes(['admin', 'write:users']); // throws if either scope is missing
|
|
236
|
+
|
|
237
|
+
const identity = getIdentity() as { sub: string };
|
|
238
|
+
// proceed with deletion...
|
|
239
|
+
return { content: [{ type: 'text', text: `Deleted ${userId}` }] };
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Cognito encodes scopes as a space-separated string in `payload.scope` (e.g. `"openid mcp:access admin"`).
|
|
245
|
+
|
|
246
|
+
### OAuth Protected Resource Metadata
|
|
247
|
+
|
|
248
|
+
For MCP clients that support OAuth auto-discovery, add `resourceServerUrl` and `authorizationServerUrl` to expose the `/.well-known/oauth-protected-resource` endpoint (RFC 9728):
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
createMcpRouter(mcpServer, {
|
|
252
|
+
auth: {
|
|
253
|
+
cognitoUserPool: { userPoolId: '...', clientId: '...' },
|
|
254
|
+
resourceServerUrl: 'https://mcp.example.com',
|
|
255
|
+
authorizationServerUrl:
|
|
256
|
+
'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxx',
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
130
261
|
## API Reference
|
|
131
262
|
|
|
132
263
|
### `createMcpRouter(server, options?)`
|
|
@@ -135,14 +266,19 @@ Creates a Koa router configured to handle MCP protocol requests.
|
|
|
135
266
|
|
|
136
267
|
**Parameters:**
|
|
137
268
|
|
|
138
|
-
- `server` (`McpServer`)
|
|
139
|
-
- `options` (`McpRouterOptions`)
|
|
140
|
-
- `path` (`string`)
|
|
141
|
-
- `sessionIdGenerator` (`() => string`)
|
|
142
|
-
- `apiBaseUrl` (`string`)
|
|
143
|
-
- `getApiHeaders` (`(ctx: Context) => Record<string, string>`)
|
|
269
|
+
- `server` (`McpServer`) — MCP server instance with registered tools and resources
|
|
270
|
+
- `options` (`McpRouterOptions`) — Optional configuration
|
|
271
|
+
- `path` (`string`) — HTTP path for MCP endpoint (default: `'/mcp'`)
|
|
272
|
+
- `sessionIdGenerator` (`() => string`) — Session ID generator for stateful servers (default: `undefined` for stateless)
|
|
273
|
+
- `apiBaseUrl` (`string`) — Base URL prepended to relative paths in `apiCall`
|
|
274
|
+
- `getApiHeaders` (`(ctx: Context) => Record<string, string>`) — Return headers to inject into every `apiCall` for this request
|
|
275
|
+
- `auth` (`McpAuthOptions`) — OAuth/JWT authentication; see [Authentication](#authentication)
|
|
276
|
+
- `auth.cognitoUserPool` — Cognito user pool config (`userPoolId`, `clientId`, `tokenUse`)
|
|
277
|
+
- `auth.verifyToken` — Custom async token verifier `(token: string) => Promise<unknown>`
|
|
278
|
+
- `auth.requiredScopes` — Router-level scope guard; returns 403 if any scope is missing
|
|
279
|
+
- `auth.resourceServerUrl` + `auth.authorizationServerUrl` — Enable `/.well-known/oauth-protected-resource`
|
|
144
280
|
|
|
145
|
-
**Returns:** `Router`
|
|
281
|
+
**Returns:** `Router` — Koa router instance
|
|
146
282
|
|
|
147
283
|
### `apiCall(method, url, options?)`
|
|
148
284
|
|
|
@@ -150,12 +286,26 @@ Generic HTTP helper for use inside MCP tool handlers.
|
|
|
150
286
|
|
|
151
287
|
**Parameters:**
|
|
152
288
|
|
|
153
|
-
- `method` (`string`)
|
|
154
|
-
- `url` (`string`)
|
|
155
|
-
- `options.body` (`unknown`, optional)
|
|
156
|
-
- `options.headers` (`Record<string, string>`, optional)
|
|
289
|
+
- `method` (`string`) — HTTP method (`'GET'`, `'POST'`, `'PUT'`, `'DELETE'`, …)
|
|
290
|
+
- `url` (`string`) — Full URL **or** a path starting with `/` (prepended with `apiBaseUrl`)
|
|
291
|
+
- `options.body` (`unknown`, optional) — Request body, serialised as JSON
|
|
292
|
+
- `options.headers` (`Record<string, string>`, optional) — Per-call header overrides; merged on top of context-injected headers
|
|
293
|
+
|
|
294
|
+
**Returns:** `Promise<unknown>` — Parsed JSON response body
|
|
295
|
+
|
|
296
|
+
### `getIdentity()`
|
|
297
|
+
|
|
298
|
+
Returns the verified JWT payload for the current MCP request. Only available inside a tool handler when `auth` is configured. Returns `undefined` when called outside an authenticated context.
|
|
299
|
+
|
|
300
|
+
**Returns:** `unknown` — Verified token payload (cast to your expected shape)
|
|
301
|
+
|
|
302
|
+
### `checkScopes(required)`
|
|
303
|
+
|
|
304
|
+
Asserts that the current request token contains all required scopes. Throws `Error: Insufficient scopes. Required: …` if any scope is missing — the MCP SDK catches this and returns a tool error to the client.
|
|
305
|
+
|
|
306
|
+
**Parameters:**
|
|
157
307
|
|
|
158
|
-
|
|
308
|
+
- `required` (`string[]`) — Scope strings that must all be present in `payload.scope`
|
|
159
309
|
|
|
160
310
|
### `registerToolFromSchema(server, params)`
|
|
161
311
|
|
|
@@ -165,11 +315,11 @@ Use this when tool definitions are shared between the MCP server and an AI SDK a
|
|
|
165
315
|
|
|
166
316
|
**Parameters:**
|
|
167
317
|
|
|
168
|
-
- `server` (`McpServer`)
|
|
169
|
-
- `params.name` (`string`)
|
|
170
|
-
- `params.description` (`string`, optional)
|
|
171
|
-
- `params.inputSchema` (`JsonObjectSchema`, optional)
|
|
172
|
-
- `params.handler` (`(args: Record<string, unknown>) => CallToolResult | Promise<CallToolResult>`)
|
|
318
|
+
- `server` (`McpServer`) — The MCP server instance
|
|
319
|
+
- `params.name` (`string`) — Unique tool name
|
|
320
|
+
- `params.description` (`string`, optional) — Human-readable description
|
|
321
|
+
- `params.inputSchema` (`JsonObjectSchema`, optional) — Plain JSON Schema object (defaults to `{ type: 'object', properties: {} }`)
|
|
322
|
+
- `params.handler` (`(args: Record<string, unknown>) => CallToolResult | Promise<CallToolResult>`) — Tool handler receiving the raw request arguments
|
|
173
323
|
|
|
174
324
|
**Returns:** `void`
|
|
175
325
|
|
package/dist/index.cjs
CHANGED
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, Symbol.toStringTag, {
|
|
|
4
4
|
});
|
|
5
5
|
let node_async_hooks = require("node:async_hooks");
|
|
6
6
|
let _modelcontextprotocol_sdk_server_streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
7
|
+
let _ttoss_auth_core_amazon_cognito = require("@ttoss/auth-core/amazon-cognito");
|
|
7
8
|
let _ttoss_http_server = require("@ttoss/http-server");
|
|
8
9
|
let zod = require("zod");
|
|
9
10
|
let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
@@ -97,6 +98,80 @@ const apiCall = async (method, url, options) => {
|
|
|
97
98
|
return response.text();
|
|
98
99
|
};
|
|
99
100
|
/**
|
|
101
|
+
* Returns the verified JWT payload for the current MCP request.
|
|
102
|
+
* Only available inside a tool handler when `auth` is configured on the router.
|
|
103
|
+
*/
|
|
104
|
+
const getIdentity = () => {
|
|
105
|
+
return requestContextStore.getStore()?.identity;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Asserts that the current request's token contains all required scopes.
|
|
109
|
+
* Throws if any scope is missing — the MCP SDK catches this and returns a
|
|
110
|
+
* tool error to the client. Use inside tool handlers for per-tool authorization.
|
|
111
|
+
*
|
|
112
|
+
* Cognito tokens carry scopes as a space-separated string in `payload.scope`.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* server.tool('delete-user', schema, async (args) => {
|
|
117
|
+
* checkScopes(['admin', 'write:users']);
|
|
118
|
+
* // proceed only if caller has both scopes
|
|
119
|
+
* });
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
const checkScopes = required => {
|
|
123
|
+
const tokenScopes = (getIdentity()?.scope ?? "").split(" ");
|
|
124
|
+
if (required.filter(s => {
|
|
125
|
+
return !tokenScopes.includes(s);
|
|
126
|
+
}).length > 0) throw new Error(`Insufficient scopes. Required: ${required.join(", ")}`);
|
|
127
|
+
};
|
|
128
|
+
const buildTokenVerifier = auth => {
|
|
129
|
+
if (auth.cognitoUserPool) {
|
|
130
|
+
const v = _ttoss_auth_core_amazon_cognito.CognitoJwtVerifier.create({
|
|
131
|
+
tokenUse: "access",
|
|
132
|
+
...auth.cognitoUserPool
|
|
133
|
+
});
|
|
134
|
+
return t => {
|
|
135
|
+
return v.verify(t);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (auth.verifyToken) return auth.verifyToken;
|
|
139
|
+
throw new Error("McpAuthOptions requires either cognitoUserPool or verifyToken");
|
|
140
|
+
};
|
|
141
|
+
const registerAuthRoutes = (router, path, auth, tokenVerifier) => {
|
|
142
|
+
router.use(path, async (ctx, next) => {
|
|
143
|
+
const authHeader = ctx.headers.authorization;
|
|
144
|
+
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
145
|
+
let identity;
|
|
146
|
+
try {
|
|
147
|
+
identity = await tokenVerifier(token);
|
|
148
|
+
} catch {
|
|
149
|
+
ctx.status = 401;
|
|
150
|
+
ctx.set("WWW-Authenticate", "Bearer");
|
|
151
|
+
ctx.body = "Unauthorized";
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (auth.requiredScopes?.length) {
|
|
155
|
+
const tokenScopes = (identity?.scope ?? "").split(" ");
|
|
156
|
+
if (auth.requiredScopes.filter(s => {
|
|
157
|
+
return !tokenScopes.includes(s);
|
|
158
|
+
}).length > 0) {
|
|
159
|
+
ctx.status = 403;
|
|
160
|
+
ctx.body = "Forbidden";
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
ctx.state.identity = identity;
|
|
165
|
+
await next();
|
|
166
|
+
});
|
|
167
|
+
if (auth.resourceServerUrl && auth.authorizationServerUrl) router.get("/.well-known/oauth-protected-resource", ctx => {
|
|
168
|
+
ctx.body = {
|
|
169
|
+
resource: auth.resourceServerUrl,
|
|
170
|
+
authorization_servers: [auth.authorizationServerUrl]
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
100
175
|
* Creates a Koa router configured to handle MCP protocol requests
|
|
101
176
|
*
|
|
102
177
|
* @param server - The MCP server instance with registered tools and resources
|
|
@@ -138,10 +213,11 @@ const createMcpRouter = (server, options = {}) => {
|
|
|
138
213
|
path = "/mcp",
|
|
139
214
|
sessionIdGenerator,
|
|
140
215
|
apiBaseUrl,
|
|
141
|
-
getApiHeaders
|
|
216
|
+
getApiHeaders,
|
|
217
|
+
auth
|
|
142
218
|
} = options;
|
|
143
219
|
const isStateful = sessionIdGenerator !== void 0;
|
|
144
|
-
const needsContext = apiBaseUrl !== void 0 || getApiHeaders !== void 0;
|
|
220
|
+
const needsContext = apiBaseUrl !== void 0 || getApiHeaders !== void 0 || auth !== void 0;
|
|
145
221
|
let sharedTransport;
|
|
146
222
|
if (isStateful) {
|
|
147
223
|
sharedTransport = new _modelcontextprotocol_sdk_server_streamableHttp_js.StreamableHTTPServerTransport({
|
|
@@ -159,8 +235,10 @@ const createMcpRouter = (server, options = {}) => {
|
|
|
159
235
|
return result;
|
|
160
236
|
};
|
|
161
237
|
const router = new _ttoss_http_server.Router();
|
|
238
|
+
if (auth) registerAuthRoutes(router, path, auth, buildTokenVerifier(auth));
|
|
162
239
|
const handleWithContext = async (ctx, body) => {
|
|
163
240
|
const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
|
|
241
|
+
const identity = ctx.state.identity;
|
|
164
242
|
const runRequest = async transport => {
|
|
165
243
|
await transport.handleRequest(ctx.req, ctx.res, body);
|
|
166
244
|
ctx.respond = false;
|
|
@@ -168,7 +246,8 @@ const createMcpRouter = (server, options = {}) => {
|
|
|
168
246
|
if (isStateful && sharedTransport) {
|
|
169
247
|
if (needsContext) await requestContextStore.run({
|
|
170
248
|
apiBaseUrl,
|
|
171
|
-
apiHeaders
|
|
249
|
+
apiHeaders,
|
|
250
|
+
identity
|
|
172
251
|
}, () => {
|
|
173
252
|
return runRequest(sharedTransport);
|
|
174
253
|
});else await runRequest(sharedTransport);
|
|
@@ -181,7 +260,8 @@ const createMcpRouter = (server, options = {}) => {
|
|
|
181
260
|
await server.connect(transport);
|
|
182
261
|
if (needsContext) await requestContextStore.run({
|
|
183
262
|
apiBaseUrl,
|
|
184
|
-
apiHeaders
|
|
263
|
+
apiHeaders,
|
|
264
|
+
identity
|
|
185
265
|
}, () => {
|
|
186
266
|
return runRequest(transport);
|
|
187
267
|
});else await runRequest(transport);
|
|
@@ -309,7 +389,9 @@ Object.defineProperty(exports, 'McpServer', {
|
|
|
309
389
|
}
|
|
310
390
|
});
|
|
311
391
|
exports.apiCall = apiCall;
|
|
392
|
+
exports.checkScopes = checkScopes;
|
|
312
393
|
exports.createMcpRouter = createMcpRouter;
|
|
394
|
+
exports.getIdentity = getIdentity;
|
|
313
395
|
exports.registerToolFromSchema = registerToolFromSchema;
|
|
314
396
|
Object.defineProperty(exports, 'z', {
|
|
315
397
|
enumerable: true,
|
package/dist/index.d.cts
CHANGED
|
@@ -737,6 +737,79 @@ interface ApiCallOptions {
|
|
|
737
737
|
* ```
|
|
738
738
|
*/
|
|
739
739
|
declare const apiCall: (method: string, url: string, options?: ApiCallOptions) => Promise<unknown>;
|
|
740
|
+
/**
|
|
741
|
+
* Returns the verified JWT payload for the current MCP request.
|
|
742
|
+
* Only available inside a tool handler when `auth` is configured on the router.
|
|
743
|
+
*/
|
|
744
|
+
declare const getIdentity: () => unknown;
|
|
745
|
+
/**
|
|
746
|
+
* Asserts that the current request's token contains all required scopes.
|
|
747
|
+
* Throws if any scope is missing — the MCP SDK catches this and returns a
|
|
748
|
+
* tool error to the client. Use inside tool handlers for per-tool authorization.
|
|
749
|
+
*
|
|
750
|
+
* Cognito tokens carry scopes as a space-separated string in `payload.scope`.
|
|
751
|
+
*
|
|
752
|
+
* @example
|
|
753
|
+
* ```typescript
|
|
754
|
+
* server.tool('delete-user', schema, async (args) => {
|
|
755
|
+
* checkScopes(['admin', 'write:users']);
|
|
756
|
+
* // proceed only if caller has both scopes
|
|
757
|
+
* });
|
|
758
|
+
* ```
|
|
759
|
+
*/
|
|
760
|
+
declare const checkScopes: (required: string[]) => void;
|
|
761
|
+
/** Amazon Cognito user pool configuration for JWT verification. */
|
|
762
|
+
interface CognitoUserPoolConfig {
|
|
763
|
+
/** The Cognito User Pool ID (e.g. `us-east-1_abc123`). */
|
|
764
|
+
userPoolId: string;
|
|
765
|
+
/**
|
|
766
|
+
* Which token type to verify.
|
|
767
|
+
* @default 'access'
|
|
768
|
+
*/
|
|
769
|
+
tokenUse?: 'access' | 'id';
|
|
770
|
+
/** The app client ID registered in the User Pool. */
|
|
771
|
+
clientId: string;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Authentication options for the MCP router.
|
|
775
|
+
*
|
|
776
|
+
* Supply either `cognitoUserPool` (uses `CognitoJwtVerifier` from
|
|
777
|
+
* `@ttoss/auth-core`) or a custom `verifyToken` function — not both.
|
|
778
|
+
*/
|
|
779
|
+
interface McpAuthOptions {
|
|
780
|
+
/**
|
|
781
|
+
* Amazon Cognito user pool config. When provided, the router creates a
|
|
782
|
+
* `CognitoJwtVerifier` and validates every incoming Bearer token against it.
|
|
783
|
+
*/
|
|
784
|
+
cognitoUserPool?: CognitoUserPoolConfig;
|
|
785
|
+
/**
|
|
786
|
+
* Custom token verifier for non-Cognito providers (Auth0, Keycloak, …).
|
|
787
|
+
* Receives the raw Bearer token string. Should resolve with the verified
|
|
788
|
+
* payload or reject/throw on failure.
|
|
789
|
+
*/
|
|
790
|
+
verifyToken?: (token: string) => Promise<unknown>;
|
|
791
|
+
/**
|
|
792
|
+
* Router-level scope guard. All listed scopes must be present on the token
|
|
793
|
+
* for any MCP request to be allowed. Returns 403 if any scope is missing.
|
|
794
|
+
*
|
|
795
|
+
* Cognito encodes scopes as a space-separated string in `payload.scope`.
|
|
796
|
+
*
|
|
797
|
+
* @example ['mcp:access']
|
|
798
|
+
*/
|
|
799
|
+
requiredScopes?: string[];
|
|
800
|
+
/**
|
|
801
|
+
* URL of this MCP server, used in the OAuth Protected Resource Metadata
|
|
802
|
+
* response (`/.well-known/oauth-protected-resource`). Both this field and
|
|
803
|
+
* `authorizationServerUrl` must be provided to enable the endpoint.
|
|
804
|
+
*/
|
|
805
|
+
resourceServerUrl?: string;
|
|
806
|
+
/**
|
|
807
|
+
* URL of the OAuth Authorization Server that issues tokens for this resource.
|
|
808
|
+
* Enables `/.well-known/oauth-protected-resource` for MCP client auto-discovery
|
|
809
|
+
* (RFC 9728) when combined with `resourceServerUrl`.
|
|
810
|
+
*/
|
|
811
|
+
authorizationServerUrl?: string;
|
|
812
|
+
}
|
|
740
813
|
/**
|
|
741
814
|
* Options for configuring the MCP router
|
|
742
815
|
*/
|
|
@@ -786,6 +859,38 @@ interface McpRouterOptions {
|
|
|
786
859
|
* ```
|
|
787
860
|
*/
|
|
788
861
|
getApiHeaders?: (ctx: Context$1) => Record<string, string>;
|
|
862
|
+
/**
|
|
863
|
+
* OAuth / JWT authentication configuration for the MCP endpoint.
|
|
864
|
+
*
|
|
865
|
+
* When set, every incoming MCP request must include a valid Bearer token in
|
|
866
|
+
* the `Authorization` header. Invalid or missing tokens receive a `401`
|
|
867
|
+
* response with `WWW-Authenticate: Bearer`. Tokens that fail a
|
|
868
|
+
* `requiredScopes` check receive `403`.
|
|
869
|
+
*
|
|
870
|
+
* The verified token payload is accessible inside tool handlers via
|
|
871
|
+
* {@link getIdentity}. Fine-grained per-tool scope checks can be done with
|
|
872
|
+
* {@link checkScopes}.
|
|
873
|
+
*
|
|
874
|
+
* @example Cognito
|
|
875
|
+
* ```typescript
|
|
876
|
+
* createMcpRouter(server, {
|
|
877
|
+
* auth: {
|
|
878
|
+
* cognitoUserPool: { userPoolId: 'us-east-1_xxx', clientId: 'yyy' },
|
|
879
|
+
* requiredScopes: ['mcp:access'],
|
|
880
|
+
* },
|
|
881
|
+
* });
|
|
882
|
+
* ```
|
|
883
|
+
*
|
|
884
|
+
* @example Custom verifier
|
|
885
|
+
* ```typescript
|
|
886
|
+
* createMcpRouter(server, {
|
|
887
|
+
* auth: {
|
|
888
|
+
* verifyToken: async (token) => myJwtLib.verify(token),
|
|
889
|
+
* },
|
|
890
|
+
* });
|
|
891
|
+
* ```
|
|
892
|
+
*/
|
|
893
|
+
auth?: McpAuthOptions;
|
|
789
894
|
}
|
|
790
895
|
/**
|
|
791
896
|
* Creates a Koa router configured to handle MCP protocol requests
|
|
@@ -902,4 +1007,4 @@ interface RegisterToolFromSchemaParams {
|
|
|
902
1007
|
*/
|
|
903
1008
|
declare const registerToolFromSchema: (server: McpServer$1, params: RegisterToolFromSchemaParams) => void;
|
|
904
1009
|
//#endregion
|
|
905
|
-
export { ApiCallOptions, JsonObjectSchema, McpRouterOptions, type McpServer, RegisterToolFromSchemaParams, apiCall, createMcpRouter, registerToolFromSchema, z };
|
|
1010
|
+
export { ApiCallOptions, CognitoUserPoolConfig, JsonObjectSchema, McpAuthOptions, McpRouterOptions, type McpServer, RegisterToolFromSchemaParams, apiCall, checkScopes, createMcpRouter, getIdentity, registerToolFromSchema, z };
|
package/dist/index.d.mts
CHANGED
|
@@ -737,6 +737,79 @@ interface ApiCallOptions {
|
|
|
737
737
|
* ```
|
|
738
738
|
*/
|
|
739
739
|
declare const apiCall: (method: string, url: string, options?: ApiCallOptions) => Promise<unknown>;
|
|
740
|
+
/**
|
|
741
|
+
* Returns the verified JWT payload for the current MCP request.
|
|
742
|
+
* Only available inside a tool handler when `auth` is configured on the router.
|
|
743
|
+
*/
|
|
744
|
+
declare const getIdentity: () => unknown;
|
|
745
|
+
/**
|
|
746
|
+
* Asserts that the current request's token contains all required scopes.
|
|
747
|
+
* Throws if any scope is missing — the MCP SDK catches this and returns a
|
|
748
|
+
* tool error to the client. Use inside tool handlers for per-tool authorization.
|
|
749
|
+
*
|
|
750
|
+
* Cognito tokens carry scopes as a space-separated string in `payload.scope`.
|
|
751
|
+
*
|
|
752
|
+
* @example
|
|
753
|
+
* ```typescript
|
|
754
|
+
* server.tool('delete-user', schema, async (args) => {
|
|
755
|
+
* checkScopes(['admin', 'write:users']);
|
|
756
|
+
* // proceed only if caller has both scopes
|
|
757
|
+
* });
|
|
758
|
+
* ```
|
|
759
|
+
*/
|
|
760
|
+
declare const checkScopes: (required: string[]) => void;
|
|
761
|
+
/** Amazon Cognito user pool configuration for JWT verification. */
|
|
762
|
+
interface CognitoUserPoolConfig {
|
|
763
|
+
/** The Cognito User Pool ID (e.g. `us-east-1_abc123`). */
|
|
764
|
+
userPoolId: string;
|
|
765
|
+
/**
|
|
766
|
+
* Which token type to verify.
|
|
767
|
+
* @default 'access'
|
|
768
|
+
*/
|
|
769
|
+
tokenUse?: 'access' | 'id';
|
|
770
|
+
/** The app client ID registered in the User Pool. */
|
|
771
|
+
clientId: string;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Authentication options for the MCP router.
|
|
775
|
+
*
|
|
776
|
+
* Supply either `cognitoUserPool` (uses `CognitoJwtVerifier` from
|
|
777
|
+
* `@ttoss/auth-core`) or a custom `verifyToken` function — not both.
|
|
778
|
+
*/
|
|
779
|
+
interface McpAuthOptions {
|
|
780
|
+
/**
|
|
781
|
+
* Amazon Cognito user pool config. When provided, the router creates a
|
|
782
|
+
* `CognitoJwtVerifier` and validates every incoming Bearer token against it.
|
|
783
|
+
*/
|
|
784
|
+
cognitoUserPool?: CognitoUserPoolConfig;
|
|
785
|
+
/**
|
|
786
|
+
* Custom token verifier for non-Cognito providers (Auth0, Keycloak, …).
|
|
787
|
+
* Receives the raw Bearer token string. Should resolve with the verified
|
|
788
|
+
* payload or reject/throw on failure.
|
|
789
|
+
*/
|
|
790
|
+
verifyToken?: (token: string) => Promise<unknown>;
|
|
791
|
+
/**
|
|
792
|
+
* Router-level scope guard. All listed scopes must be present on the token
|
|
793
|
+
* for any MCP request to be allowed. Returns 403 if any scope is missing.
|
|
794
|
+
*
|
|
795
|
+
* Cognito encodes scopes as a space-separated string in `payload.scope`.
|
|
796
|
+
*
|
|
797
|
+
* @example ['mcp:access']
|
|
798
|
+
*/
|
|
799
|
+
requiredScopes?: string[];
|
|
800
|
+
/**
|
|
801
|
+
* URL of this MCP server, used in the OAuth Protected Resource Metadata
|
|
802
|
+
* response (`/.well-known/oauth-protected-resource`). Both this field and
|
|
803
|
+
* `authorizationServerUrl` must be provided to enable the endpoint.
|
|
804
|
+
*/
|
|
805
|
+
resourceServerUrl?: string;
|
|
806
|
+
/**
|
|
807
|
+
* URL of the OAuth Authorization Server that issues tokens for this resource.
|
|
808
|
+
* Enables `/.well-known/oauth-protected-resource` for MCP client auto-discovery
|
|
809
|
+
* (RFC 9728) when combined with `resourceServerUrl`.
|
|
810
|
+
*/
|
|
811
|
+
authorizationServerUrl?: string;
|
|
812
|
+
}
|
|
740
813
|
/**
|
|
741
814
|
* Options for configuring the MCP router
|
|
742
815
|
*/
|
|
@@ -786,6 +859,38 @@ interface McpRouterOptions {
|
|
|
786
859
|
* ```
|
|
787
860
|
*/
|
|
788
861
|
getApiHeaders?: (ctx: Context$1) => Record<string, string>;
|
|
862
|
+
/**
|
|
863
|
+
* OAuth / JWT authentication configuration for the MCP endpoint.
|
|
864
|
+
*
|
|
865
|
+
* When set, every incoming MCP request must include a valid Bearer token in
|
|
866
|
+
* the `Authorization` header. Invalid or missing tokens receive a `401`
|
|
867
|
+
* response with `WWW-Authenticate: Bearer`. Tokens that fail a
|
|
868
|
+
* `requiredScopes` check receive `403`.
|
|
869
|
+
*
|
|
870
|
+
* The verified token payload is accessible inside tool handlers via
|
|
871
|
+
* {@link getIdentity}. Fine-grained per-tool scope checks can be done with
|
|
872
|
+
* {@link checkScopes}.
|
|
873
|
+
*
|
|
874
|
+
* @example Cognito
|
|
875
|
+
* ```typescript
|
|
876
|
+
* createMcpRouter(server, {
|
|
877
|
+
* auth: {
|
|
878
|
+
* cognitoUserPool: { userPoolId: 'us-east-1_xxx', clientId: 'yyy' },
|
|
879
|
+
* requiredScopes: ['mcp:access'],
|
|
880
|
+
* },
|
|
881
|
+
* });
|
|
882
|
+
* ```
|
|
883
|
+
*
|
|
884
|
+
* @example Custom verifier
|
|
885
|
+
* ```typescript
|
|
886
|
+
* createMcpRouter(server, {
|
|
887
|
+
* auth: {
|
|
888
|
+
* verifyToken: async (token) => myJwtLib.verify(token),
|
|
889
|
+
* },
|
|
890
|
+
* });
|
|
891
|
+
* ```
|
|
892
|
+
*/
|
|
893
|
+
auth?: McpAuthOptions;
|
|
789
894
|
}
|
|
790
895
|
/**
|
|
791
896
|
* Creates a Koa router configured to handle MCP protocol requests
|
|
@@ -902,4 +1007,4 @@ interface RegisterToolFromSchemaParams {
|
|
|
902
1007
|
*/
|
|
903
1008
|
declare const registerToolFromSchema: (server: McpServer$1, params: RegisterToolFromSchemaParams) => void;
|
|
904
1009
|
//#endregion
|
|
905
|
-
export { ApiCallOptions, JsonObjectSchema, McpRouterOptions, type McpServer, RegisterToolFromSchemaParams, apiCall, createMcpRouter, registerToolFromSchema, z };
|
|
1010
|
+
export { ApiCallOptions, CognitoUserPoolConfig, JsonObjectSchema, McpAuthOptions, McpRouterOptions, type McpServer, RegisterToolFromSchemaParams, apiCall, checkScopes, createMcpRouter, getIdentity, registerToolFromSchema, z };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
3
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { CognitoJwtVerifier } from "@ttoss/auth-core/amazon-cognito";
|
|
4
5
|
import { Router } from "@ttoss/http-server";
|
|
5
6
|
import { z, z as z$1 } from "zod";
|
|
6
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -94,6 +95,80 @@ const apiCall = async (method, url, options) => {
|
|
|
94
95
|
return response.text();
|
|
95
96
|
};
|
|
96
97
|
/**
|
|
98
|
+
* Returns the verified JWT payload for the current MCP request.
|
|
99
|
+
* Only available inside a tool handler when `auth` is configured on the router.
|
|
100
|
+
*/
|
|
101
|
+
const getIdentity = () => {
|
|
102
|
+
return requestContextStore.getStore()?.identity;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Asserts that the current request's token contains all required scopes.
|
|
106
|
+
* Throws if any scope is missing — the MCP SDK catches this and returns a
|
|
107
|
+
* tool error to the client. Use inside tool handlers for per-tool authorization.
|
|
108
|
+
*
|
|
109
|
+
* Cognito tokens carry scopes as a space-separated string in `payload.scope`.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* server.tool('delete-user', schema, async (args) => {
|
|
114
|
+
* checkScopes(['admin', 'write:users']);
|
|
115
|
+
* // proceed only if caller has both scopes
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
const checkScopes = required => {
|
|
120
|
+
const tokenScopes = (getIdentity()?.scope ?? "").split(" ");
|
|
121
|
+
if (required.filter(s => {
|
|
122
|
+
return !tokenScopes.includes(s);
|
|
123
|
+
}).length > 0) throw new Error(`Insufficient scopes. Required: ${required.join(", ")}`);
|
|
124
|
+
};
|
|
125
|
+
const buildTokenVerifier = auth => {
|
|
126
|
+
if (auth.cognitoUserPool) {
|
|
127
|
+
const v = CognitoJwtVerifier.create({
|
|
128
|
+
tokenUse: "access",
|
|
129
|
+
...auth.cognitoUserPool
|
|
130
|
+
});
|
|
131
|
+
return t => {
|
|
132
|
+
return v.verify(t);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (auth.verifyToken) return auth.verifyToken;
|
|
136
|
+
throw new Error("McpAuthOptions requires either cognitoUserPool or verifyToken");
|
|
137
|
+
};
|
|
138
|
+
const registerAuthRoutes = (router, path, auth, tokenVerifier) => {
|
|
139
|
+
router.use(path, async (ctx, next) => {
|
|
140
|
+
const authHeader = ctx.headers.authorization;
|
|
141
|
+
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
142
|
+
let identity;
|
|
143
|
+
try {
|
|
144
|
+
identity = await tokenVerifier(token);
|
|
145
|
+
} catch {
|
|
146
|
+
ctx.status = 401;
|
|
147
|
+
ctx.set("WWW-Authenticate", "Bearer");
|
|
148
|
+
ctx.body = "Unauthorized";
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (auth.requiredScopes?.length) {
|
|
152
|
+
const tokenScopes = (identity?.scope ?? "").split(" ");
|
|
153
|
+
if (auth.requiredScopes.filter(s => {
|
|
154
|
+
return !tokenScopes.includes(s);
|
|
155
|
+
}).length > 0) {
|
|
156
|
+
ctx.status = 403;
|
|
157
|
+
ctx.body = "Forbidden";
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
ctx.state.identity = identity;
|
|
162
|
+
await next();
|
|
163
|
+
});
|
|
164
|
+
if (auth.resourceServerUrl && auth.authorizationServerUrl) router.get("/.well-known/oauth-protected-resource", ctx => {
|
|
165
|
+
ctx.body = {
|
|
166
|
+
resource: auth.resourceServerUrl,
|
|
167
|
+
authorization_servers: [auth.authorizationServerUrl]
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
97
172
|
* Creates a Koa router configured to handle MCP protocol requests
|
|
98
173
|
*
|
|
99
174
|
* @param server - The MCP server instance with registered tools and resources
|
|
@@ -135,10 +210,11 @@ const createMcpRouter = (server, options = {}) => {
|
|
|
135
210
|
path = "/mcp",
|
|
136
211
|
sessionIdGenerator,
|
|
137
212
|
apiBaseUrl,
|
|
138
|
-
getApiHeaders
|
|
213
|
+
getApiHeaders,
|
|
214
|
+
auth
|
|
139
215
|
} = options;
|
|
140
216
|
const isStateful = sessionIdGenerator !== void 0;
|
|
141
|
-
const needsContext = apiBaseUrl !== void 0 || getApiHeaders !== void 0;
|
|
217
|
+
const needsContext = apiBaseUrl !== void 0 || getApiHeaders !== void 0 || auth !== void 0;
|
|
142
218
|
let sharedTransport;
|
|
143
219
|
if (isStateful) {
|
|
144
220
|
sharedTransport = new StreamableHTTPServerTransport({
|
|
@@ -156,8 +232,10 @@ const createMcpRouter = (server, options = {}) => {
|
|
|
156
232
|
return result;
|
|
157
233
|
};
|
|
158
234
|
const router = new Router();
|
|
235
|
+
if (auth) registerAuthRoutes(router, path, auth, buildTokenVerifier(auth));
|
|
159
236
|
const handleWithContext = async (ctx, body) => {
|
|
160
237
|
const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
|
|
238
|
+
const identity = ctx.state.identity;
|
|
161
239
|
const runRequest = async transport => {
|
|
162
240
|
await transport.handleRequest(ctx.req, ctx.res, body);
|
|
163
241
|
ctx.respond = false;
|
|
@@ -165,7 +243,8 @@ const createMcpRouter = (server, options = {}) => {
|
|
|
165
243
|
if (isStateful && sharedTransport) {
|
|
166
244
|
if (needsContext) await requestContextStore.run({
|
|
167
245
|
apiBaseUrl,
|
|
168
|
-
apiHeaders
|
|
246
|
+
apiHeaders,
|
|
247
|
+
identity
|
|
169
248
|
}, () => {
|
|
170
249
|
return runRequest(sharedTransport);
|
|
171
250
|
});else await runRequest(sharedTransport);
|
|
@@ -178,7 +257,8 @@ const createMcpRouter = (server, options = {}) => {
|
|
|
178
257
|
await server.connect(transport);
|
|
179
258
|
if (needsContext) await requestContextStore.run({
|
|
180
259
|
apiBaseUrl,
|
|
181
|
-
apiHeaders
|
|
260
|
+
apiHeaders,
|
|
261
|
+
identity
|
|
182
262
|
}, () => {
|
|
183
263
|
return runRequest(transport);
|
|
184
264
|
});else await runRequest(transport);
|
|
@@ -299,4 +379,4 @@ const registerToolFromSchema = (server, params) => {
|
|
|
299
379
|
};
|
|
300
380
|
|
|
301
381
|
//#endregion
|
|
302
|
-
export { McpServer, apiCall, createMcpRouter, registerToolFromSchema, z };
|
|
382
|
+
export { McpServer, apiCall, checkScopes, createMcpRouter, getIdentity, registerToolFromSchema, z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttoss/http-server-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
37
|
"zod": "^4.3.6",
|
|
38
|
+
"@ttoss/auth-core": "^0.4.13",
|
|
38
39
|
"@ttoss/http-server": "^0.5.14"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|