@ttoss/http-server-mcp 0.13.8 → 0.14.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 CHANGED
@@ -169,7 +169,7 @@ const mcpRouter = createMcpRouter(mcpServer, {
169
169
 
170
170
  ### Custom verifier
171
171
 
172
- Pass an async `verifyToken` function for any other provider (Auth0, Keycloak, etc.):
172
+ Pass an async `verifyToken` function for any provider JWT-based or opaque. The contract is simply: resolve with an identity payload on success, or throw on failure.
173
173
 
174
174
  ```typescript
175
175
  import { createMcpRouter } from '@ttoss/http-server-mcp';
@@ -189,6 +189,25 @@ const mcpRouter = createMcpRouter(mcpServer, {
189
189
  });
190
190
  ```
191
191
 
192
+ **Opaque token (database lookup):** `verifyToken` does not have to be JWT-based — a plain API-key lookup works equally well:
193
+
194
+ ```typescript
195
+ const mcpRouter = createMcpRouter(mcpServer, {
196
+ auth: {
197
+ verifyToken: async (token) => {
198
+ // Look up the hashed token in your database
199
+ const record = await db.apiKeys.findByHash(sha256(token));
200
+ if (!record || record.revokedAt) {
201
+ throw new Error('Invalid API key');
202
+ }
203
+ return { sub: record.userId, scope: record.scopes.join(' ') };
204
+ },
205
+ },
206
+ });
207
+ ```
208
+
209
+ The router emits `401 Unauthorized` whenever `verifyToken` throws, regardless of whether you are using JWTs or opaque tokens.
210
+
192
211
  ### Accessing the verified identity
193
212
 
194
213
  Inside any tool handler, call `getIdentity()` to retrieve the verified JWT payload:
@@ -245,7 +264,9 @@ Cognito encodes scopes as a space-separated string in `payload.scope` (e.g. `"op
245
264
 
246
265
  ### OAuth Protected Resource Metadata
247
266
 
248
- For MCP clients that support OAuth auto-discovery, add `resourceServerUrl` and `authorizationServerUrl` to expose the `/.well-known/oauth-protected-resource` endpoint (RFC 9728):
267
+ MCP clients (Claude, Cursor, etc.) fetch `/.well-known/oauth-protected-resource` to discover which authorization server issues tokens for your MCP server. The endpoint must be **unauthenticated** — MCP clients call it before they have a token.
268
+
269
+ **With the built-in `auth` option** — add `resourceServerUrl` and `authorizationServerUrl`:
249
270
 
250
271
  ```typescript
251
272
  createMcpRouter(mcpServer, {
@@ -258,6 +279,42 @@ createMcpRouter(mcpServer, {
258
279
  });
259
280
  ```
260
281
 
282
+ **With your own auth middleware** — use `createProtectedResourceMetadataMiddleware` as a standalone middleware, mounted _before_ your auth layer so discovery stays unauthenticated:
283
+
284
+ ```typescript
285
+ import {
286
+ createProtectedResourceMetadataMiddleware,
287
+ getWwwAuthenticateHeader,
288
+ } from '@ttoss/http-server-mcp';
289
+
290
+ // Mount the discovery endpoint before your own auth middleware
291
+ app.use(
292
+ createProtectedResourceMetadataMiddleware({
293
+ resource: 'https://mcp.example.com',
294
+ authorizationServers: ['https://api.example.com'],
295
+ })
296
+ );
297
+
298
+ // Your own auth middleware — emit the spec-compliant WWW-Authenticate header on 401s
299
+ app.use(async (ctx, next) => {
300
+ const token = ctx.headers.authorization?.replace('Bearer ', '');
301
+ if (!token || !(await myVerify(token))) {
302
+ ctx.status = 401;
303
+ ctx.set(
304
+ 'WWW-Authenticate',
305
+ getWwwAuthenticateHeader({ resource: 'https://mcp.example.com' })
306
+ );
307
+ ctx.body = 'Unauthorized';
308
+ return;
309
+ }
310
+ await next();
311
+ });
312
+
313
+ app.use(createMcpRouter(mcpServer).routes());
314
+ ```
315
+
316
+ The `WWW-Authenticate: Bearer resource_metadata="…"` header is how MCP clients bootstrap OAuth discovery after their first unauthorized request.
317
+
261
318
  ## API Reference
262
319
 
263
320
  ### `createMcpRouter(server, options?)`
@@ -307,6 +364,27 @@ Asserts that the current request token contains all required scopes. Throws `Err
307
364
 
308
365
  - `required` (`string[]`) — Scope strings that must all be present in `payload.scope`
309
366
 
367
+ ### `createProtectedResourceMetadataMiddleware(args)`
368
+
369
+ Creates a standalone Koa middleware that serves `GET /.well-known/oauth-protected-resource` (RFC 9728). Use this when you have your own auth middleware and don't want to tie the discovery endpoint to the built-in `auth` option.
370
+
371
+ **Parameters:**
372
+
373
+ - `args.resource` (`string`) — The protected resource's identifier URI (your MCP server URL)
374
+ - `args.authorizationServers` (`string[]`) — Issuer URIs of the authorization servers that protect this resource
375
+
376
+ **Returns:** `Koa.Middleware`
377
+
378
+ ### `getWwwAuthenticateHeader(args)`
379
+
380
+ Returns the `WWW-Authenticate` header value for a 401 response, formatted per the MCP auth spec: `Bearer resource_metadata="<resource>/.well-known/oauth-protected-resource"`.
381
+
382
+ **Parameters:**
383
+
384
+ - `args.resource` (`string`) — The protected resource URL (trailing slash is stripped automatically)
385
+
386
+ **Returns:** `string` — The full `WWW-Authenticate` header value
387
+
310
388
  ### `registerToolFromSchema(server, params)`
311
389
 
312
390
  Registers a tool using a **plain JSON Schema** object for `inputSchema` instead of a Zod shape.
package/dist/index.cjs CHANGED
@@ -380,6 +380,62 @@ var registerToolFromSchema = (server, params) => {
380
380
  });
381
381
  }
382
382
  };
383
+ /**
384
+ * Returns the `WWW-Authenticate` header value for a 401 response on a
385
+ * protected resource, following the MCP auth spec requirement that
386
+ * unauthorized responses advertise the resource metadata URL so MCP
387
+ * clients can bootstrap OAuth discovery.
388
+ *
389
+ * Use this in your own auth middleware when you are not using the built-in
390
+ * `auth` option on `createMcpRouter`.
391
+ *
392
+ * @example
393
+ * ```typescript
394
+ * import { getWwwAuthenticateHeader } from '@ttoss/http-server-mcp';
395
+ *
396
+ * // Inside a Koa middleware
397
+ * ctx.status = 401;
398
+ * ctx.set('WWW-Authenticate', getWwwAuthenticateHeader({ resource: 'https://mcp.example.com' }));
399
+ * ```
400
+ */
401
+ var getWwwAuthenticateHeader = args => {
402
+ return `Bearer resource_metadata="${`${args.resource.replace(/\/$/, "")}/.well-known/oauth-protected-resource`}"`;
403
+ };
404
+ /**
405
+ * Creates a standalone Koa middleware that serves
406
+ * `GET /.well-known/oauth-protected-resource` (RFC 9728) without requiring
407
+ * the built-in `auth` option on `createMcpRouter`.
408
+ *
409
+ * Mount this **before** your own auth middleware so the discovery endpoint
410
+ * remains unauthenticated (MCP clients fetch it before they have a token).
411
+ *
412
+ * @example
413
+ * ```typescript
414
+ * import Koa from 'koa';
415
+ * import { createProtectedResourceMetadataMiddleware } from '@ttoss/http-server-mcp';
416
+ *
417
+ * const app = new Koa();
418
+ * app.use(
419
+ * createProtectedResourceMetadataMiddleware({
420
+ * resource: 'https://mcp.example.com',
421
+ * authorizationServers: ['https://api.example.com'],
422
+ * })
423
+ * );
424
+ * app.use(myOwnAuthMiddleware);
425
+ * ```
426
+ */
427
+ var createProtectedResourceMetadataMiddleware = args => {
428
+ return async (ctx, next) => {
429
+ if (ctx.method === "GET" && ctx.path === "/.well-known/oauth-protected-resource") {
430
+ ctx.body = {
431
+ resource: args.resource,
432
+ authorization_servers: args.authorizationServers
433
+ };
434
+ return;
435
+ }
436
+ await next();
437
+ };
438
+ };
383
439
 
384
440
  //#endregion
385
441
  Object.defineProperty(exports, 'McpServer', {
@@ -391,7 +447,9 @@ Object.defineProperty(exports, 'McpServer', {
391
447
  exports.apiCall = apiCall;
392
448
  exports.checkScopes = checkScopes;
393
449
  exports.createMcpRouter = createMcpRouter;
450
+ exports.createProtectedResourceMetadataMiddleware = createProtectedResourceMetadataMiddleware;
394
451
  exports.getIdentity = getIdentity;
452
+ exports.getWwwAuthenticateHeader = getWwwAuthenticateHeader;
395
453
  exports.registerToolFromSchema = registerToolFromSchema;
396
454
  Object.defineProperty(exports, 'z', {
397
455
  enumerable: true,
package/dist/index.d.cts CHANGED
@@ -1006,5 +1006,64 @@ interface RegisterToolFromSchemaParams {
1006
1006
  * ```
1007
1007
  */
1008
1008
  declare const registerToolFromSchema: (server: McpServer$1, params: RegisterToolFromSchemaParams) => void;
1009
+ /**
1010
+ * Returns the `WWW-Authenticate` header value for a 401 response on a
1011
+ * protected resource, following the MCP auth spec requirement that
1012
+ * unauthorized responses advertise the resource metadata URL so MCP
1013
+ * clients can bootstrap OAuth discovery.
1014
+ *
1015
+ * Use this in your own auth middleware when you are not using the built-in
1016
+ * `auth` option on `createMcpRouter`.
1017
+ *
1018
+ * @example
1019
+ * ```typescript
1020
+ * import { getWwwAuthenticateHeader } from '@ttoss/http-server-mcp';
1021
+ *
1022
+ * // Inside a Koa middleware
1023
+ * ctx.status = 401;
1024
+ * ctx.set('WWW-Authenticate', getWwwAuthenticateHeader({ resource: 'https://mcp.example.com' }));
1025
+ * ```
1026
+ */
1027
+ declare const getWwwAuthenticateHeader: (args: {
1028
+ /**
1029
+ * The resource server URL. The metadata URL is derived as
1030
+ * `<resource>/.well-known/oauth-protected-resource` per RFC 9728.
1031
+ */
1032
+ resource: string;
1033
+ }) => string;
1034
+ /**
1035
+ * Creates a standalone Koa middleware that serves
1036
+ * `GET /.well-known/oauth-protected-resource` (RFC 9728) without requiring
1037
+ * the built-in `auth` option on `createMcpRouter`.
1038
+ *
1039
+ * Mount this **before** your own auth middleware so the discovery endpoint
1040
+ * remains unauthenticated (MCP clients fetch it before they have a token).
1041
+ *
1042
+ * @example
1043
+ * ```typescript
1044
+ * import Koa from 'koa';
1045
+ * import { createProtectedResourceMetadataMiddleware } from '@ttoss/http-server-mcp';
1046
+ *
1047
+ * const app = new Koa();
1048
+ * app.use(
1049
+ * createProtectedResourceMetadataMiddleware({
1050
+ * resource: 'https://mcp.example.com',
1051
+ * authorizationServers: ['https://api.example.com'],
1052
+ * })
1053
+ * );
1054
+ * app.use(myOwnAuthMiddleware);
1055
+ * ```
1056
+ */
1057
+ declare const createProtectedResourceMetadataMiddleware: (args: {
1058
+ /**
1059
+ * The protected resource's identifier URI (the MCP server URL).
1060
+ */
1061
+ resource: string;
1062
+ /**
1063
+ * List of authorization server issuer URIs that issue tokens for this
1064
+ * resource.
1065
+ */
1066
+ authorizationServers: string[];
1067
+ }) => Application.Middleware;
1009
1068
  //#endregion
1010
- export { ApiCallOptions, CognitoUserPoolConfig, JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, RegisterToolFromSchemaParams, apiCall, checkScopes, createMcpRouter, getIdentity, registerToolFromSchema, z };
1069
+ export { ApiCallOptions, CognitoUserPoolConfig, JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, RegisterToolFromSchemaParams, apiCall, checkScopes, createMcpRouter, createProtectedResourceMetadataMiddleware, getIdentity, getWwwAuthenticateHeader, registerToolFromSchema, z };
package/dist/index.d.mts CHANGED
@@ -1006,5 +1006,64 @@ interface RegisterToolFromSchemaParams {
1006
1006
  * ```
1007
1007
  */
1008
1008
  declare const registerToolFromSchema: (server: McpServer$1, params: RegisterToolFromSchemaParams) => void;
1009
+ /**
1010
+ * Returns the `WWW-Authenticate` header value for a 401 response on a
1011
+ * protected resource, following the MCP auth spec requirement that
1012
+ * unauthorized responses advertise the resource metadata URL so MCP
1013
+ * clients can bootstrap OAuth discovery.
1014
+ *
1015
+ * Use this in your own auth middleware when you are not using the built-in
1016
+ * `auth` option on `createMcpRouter`.
1017
+ *
1018
+ * @example
1019
+ * ```typescript
1020
+ * import { getWwwAuthenticateHeader } from '@ttoss/http-server-mcp';
1021
+ *
1022
+ * // Inside a Koa middleware
1023
+ * ctx.status = 401;
1024
+ * ctx.set('WWW-Authenticate', getWwwAuthenticateHeader({ resource: 'https://mcp.example.com' }));
1025
+ * ```
1026
+ */
1027
+ declare const getWwwAuthenticateHeader: (args: {
1028
+ /**
1029
+ * The resource server URL. The metadata URL is derived as
1030
+ * `<resource>/.well-known/oauth-protected-resource` per RFC 9728.
1031
+ */
1032
+ resource: string;
1033
+ }) => string;
1034
+ /**
1035
+ * Creates a standalone Koa middleware that serves
1036
+ * `GET /.well-known/oauth-protected-resource` (RFC 9728) without requiring
1037
+ * the built-in `auth` option on `createMcpRouter`.
1038
+ *
1039
+ * Mount this **before** your own auth middleware so the discovery endpoint
1040
+ * remains unauthenticated (MCP clients fetch it before they have a token).
1041
+ *
1042
+ * @example
1043
+ * ```typescript
1044
+ * import Koa from 'koa';
1045
+ * import { createProtectedResourceMetadataMiddleware } from '@ttoss/http-server-mcp';
1046
+ *
1047
+ * const app = new Koa();
1048
+ * app.use(
1049
+ * createProtectedResourceMetadataMiddleware({
1050
+ * resource: 'https://mcp.example.com',
1051
+ * authorizationServers: ['https://api.example.com'],
1052
+ * })
1053
+ * );
1054
+ * app.use(myOwnAuthMiddleware);
1055
+ * ```
1056
+ */
1057
+ declare const createProtectedResourceMetadataMiddleware: (args: {
1058
+ /**
1059
+ * The protected resource's identifier URI (the MCP server URL).
1060
+ */
1061
+ resource: string;
1062
+ /**
1063
+ * List of authorization server issuer URIs that issue tokens for this
1064
+ * resource.
1065
+ */
1066
+ authorizationServers: string[];
1067
+ }) => Application.Middleware;
1009
1068
  //#endregion
1010
- export { ApiCallOptions, CognitoUserPoolConfig, JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, RegisterToolFromSchemaParams, apiCall, checkScopes, createMcpRouter, getIdentity, registerToolFromSchema, z };
1069
+ export { ApiCallOptions, CognitoUserPoolConfig, JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, RegisterToolFromSchemaParams, apiCall, checkScopes, createMcpRouter, createProtectedResourceMetadataMiddleware, getIdentity, getWwwAuthenticateHeader, registerToolFromSchema, z };
package/dist/index.mjs CHANGED
@@ -377,6 +377,62 @@ var registerToolFromSchema = (server, params) => {
377
377
  });
378
378
  }
379
379
  };
380
+ /**
381
+ * Returns the `WWW-Authenticate` header value for a 401 response on a
382
+ * protected resource, following the MCP auth spec requirement that
383
+ * unauthorized responses advertise the resource metadata URL so MCP
384
+ * clients can bootstrap OAuth discovery.
385
+ *
386
+ * Use this in your own auth middleware when you are not using the built-in
387
+ * `auth` option on `createMcpRouter`.
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * import { getWwwAuthenticateHeader } from '@ttoss/http-server-mcp';
392
+ *
393
+ * // Inside a Koa middleware
394
+ * ctx.status = 401;
395
+ * ctx.set('WWW-Authenticate', getWwwAuthenticateHeader({ resource: 'https://mcp.example.com' }));
396
+ * ```
397
+ */
398
+ var getWwwAuthenticateHeader = args => {
399
+ return `Bearer resource_metadata="${`${args.resource.replace(/\/$/, "")}/.well-known/oauth-protected-resource`}"`;
400
+ };
401
+ /**
402
+ * Creates a standalone Koa middleware that serves
403
+ * `GET /.well-known/oauth-protected-resource` (RFC 9728) without requiring
404
+ * the built-in `auth` option on `createMcpRouter`.
405
+ *
406
+ * Mount this **before** your own auth middleware so the discovery endpoint
407
+ * remains unauthenticated (MCP clients fetch it before they have a token).
408
+ *
409
+ * @example
410
+ * ```typescript
411
+ * import Koa from 'koa';
412
+ * import { createProtectedResourceMetadataMiddleware } from '@ttoss/http-server-mcp';
413
+ *
414
+ * const app = new Koa();
415
+ * app.use(
416
+ * createProtectedResourceMetadataMiddleware({
417
+ * resource: 'https://mcp.example.com',
418
+ * authorizationServers: ['https://api.example.com'],
419
+ * })
420
+ * );
421
+ * app.use(myOwnAuthMiddleware);
422
+ * ```
423
+ */
424
+ var createProtectedResourceMetadataMiddleware = args => {
425
+ return async (ctx, next) => {
426
+ if (ctx.method === "GET" && ctx.path === "/.well-known/oauth-protected-resource") {
427
+ ctx.body = {
428
+ resource: args.resource,
429
+ authorization_servers: args.authorizationServers
430
+ };
431
+ return;
432
+ }
433
+ await next();
434
+ };
435
+ };
380
436
 
381
437
  //#endregion
382
- export { McpServer, apiCall, checkScopes, createMcpRouter, getIdentity, registerToolFromSchema, z };
438
+ export { McpServer, apiCall, checkScopes, createMcpRouter, createProtectedResourceMetadataMiddleware, getIdentity, getWwwAuthenticateHeader, registerToolFromSchema, z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttoss/http-server-mcp",
3
- "version": "0.13.8",
3
+ "version": "0.14.0",
4
4
  "description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
5
5
  "keywords": [
6
6
  "ai",
@@ -35,8 +35,8 @@
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "^1.29.0",
37
37
  "zod": "^4.4.3",
38
- "@ttoss/auth-core": "^0.6.0",
39
- "@ttoss/http-server": "^0.6.1"
38
+ "@ttoss/http-server": "^0.6.1",
39
+ "@ttoss/auth-core": "^0.6.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/koa": "^3.0.3",