@ttoss/http-server-mcp 0.12.5 → 0.13.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/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 &lt;token&gt;
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`) - MCP server instance with registered tools and resources
139
- - `options` (`McpRouterOptions`) - Optional configuration
140
- - `path` (`string`) - HTTP path for MCP endpoint (default: `'/mcp'`)
141
- - `sessionIdGenerator` (`() => string`) - Session ID generator for stateful servers (default: `undefined` for stateless)
142
- - `apiBaseUrl` (`string`) - Base URL prepended to relative paths in `apiCall`
143
- - `getApiHeaders` (`(ctx: Context) => Record<string, string>`) - Return headers to inject into every `apiCall` for this request (auth tokens, API keys, trace headers, etc.)
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` - Koa router instance
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`) - HTTP method (`'GET'`, `'POST'`, `'PUT'`, `'DELETE'`, …)
154
- - `url` (`string`) - Full URL **or** a path starting with `/` (prepended with `apiBaseUrl`)
155
- - `options.body` (`unknown`, optional) - Request body, serialised as JSON
156
- - `options.headers` (`Record<string, string>`, optional) - Per-call header overrides; merged on top of context-injected headers
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
- **Returns:** `Promise<unknown>` - Parsed JSON response body
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`) - The MCP server instance
169
- - `params.name` (`string`) - Unique tool name
170
- - `params.description` (`string`, optional) - Human-readable description
171
- - `params.inputSchema` (`JsonObjectSchema`, optional) - Plain JSON Schema object (defaults to `{ type: 'object', properties: {} }`)
172
- - `params.handler` (`(args: Record<string, unknown>) => CallToolResult | Promise<CallToolResult>`) - Tool handler receiving the raw request arguments
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.12.5",
3
+ "version": "0.13.1",
4
4
  "description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
5
5
  "keywords": [
6
6
  "ai",
@@ -24,9 +24,9 @@
24
24
  "type": "module",
25
25
  "exports": {
26
26
  ".": {
27
- "import": "./dist/esm/index.js",
28
- "require": "./dist/index.js",
29
- "types": "./dist/index.d.ts"
27
+ "import": "./dist/index.mjs",
28
+ "require": "./dist/index.cjs",
29
+ "types": "./dist/index.d.mts"
30
30
  }
31
31
  },
32
32
  "files": [
@@ -35,7 +35,8 @@
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "^1.29.0",
37
37
  "zod": "^4.3.6",
38
- "@ttoss/http-server": "^0.5.14"
38
+ "@ttoss/auth-core": "^0.4.14",
39
+ "@ttoss/http-server": "^0.5.15"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@types/koa": "^3.0.2",