@ttoss/http-server-mcp 0.25.2 → 0.26.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
@@ -305,6 +305,17 @@ const mcpRouter = createMcpRouter(mcpServer, {
305
305
 
306
306
  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.
307
307
 
308
+ The document is served at **two** locations, both unauthenticated:
309
+
310
+ | Location | Who fetches it |
311
+ | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
312
+ | `/.well-known/oauth-protected-resource` | Clients following the `resource_metadata` value in the `WWW-Authenticate` header |
313
+ | `/.well-known/oauth-protected-resource<path>` | Clients applying RFC 9728 §3.1, which derives the URL from the resource identifier's path — e.g. `/.well-known/oauth-protected-resource/mcp` for the default mount |
314
+
315
+ Serving only the root location makes a spec-following client fail discovery outright. When `path` is `'/'` the derivation produces the root itself, so only one route is registered.
316
+
317
+ Both the locations and the document come from `protectedResourceMetadataPaths` / `protectedResourceMetadataDocument` in [`@ttoss/auth-core`](https://ttoss.dev/docs/modules/packages/auth-core), which every package here shares — so this router, `oauthServer`, `createProtectedResourceMetadataMiddleware`, and `getWwwAuthenticateHeader` cannot disagree about where the document lives.
318
+
308
319
  **With the built-in `auth` option** — add `resourceServerUrl` and `authorizationServerUrl`:
309
320
 
310
321
  ```typescript
@@ -361,25 +372,23 @@ The `WWW-Authenticate: Bearer resource_metadata="…"` header is how MCP clients
361
372
  The two behaviors the [MCP authorization spec](https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/authorization/) requires for client bootstrapping are built into the `auth` option, so you no longer need the hand-rolled middleware shown above:
362
373
 
363
374
  - **`publicMethods`** — JSON-RPC methods that bypass verification, read from the request body's `method` field. Defaults to `['initialize', 'tools/list']` so clients can discover the server before authenticating. Pass `[]` to require a token for every method, or a custom list to change the exempt set. Leaving it unset serves the full tool catalogue — every tool name, description, and input schema — to unauthenticated callers, and logs a one-time warning explaining that and how to close it. Set `publicMethods` explicitly (even to the same default) to silence the warning.
364
- - **`resourceMetadataUrl`** — when set, a `401` responds with `WWW-Authenticate: Bearer resource_metadata="<resourceMetadataUrl>"` (RFC 9728) instead of a bare `Bearer`, pointing MCP clients at the protected-resource metadata document. When omitted, the header falls back to `Bearer`.
375
+ - **`resourceMetadataUrl`** — the URL a `401` advertises as `WWW-Authenticate: Bearer resource_metadata="<url>"` (RFC 9728) instead of a bare `Bearer`, pointing MCP clients at the protected-resource metadata document. **You normally do not set it**: when the document is served — i.e. both `resourceServerUrl` and `authorizationServerUrl` are configured — it defaults to the RFC 9728 location this router serves it at, so the header and the routes cannot drift apart. Set it only to point at a document served elsewhere. When the document is not served, the header stays a bare `Bearer` rather than naming a location with no route.
365
376
 
366
377
  ```typescript
367
378
  createMcpRouter(mcpServer, {
368
379
  auth: {
369
380
  cognitoUserPool: { userPoolId: '...', clientId: '...' },
370
- // Serve the metadata document (unauthenticated)...
381
+ // Serves the metadata document (unauthenticated) and points 401s at it
382
+ // for auto-discovery — resourceMetadataUrl is derived from these two.
371
383
  resourceServerUrl: 'https://mcp.example.com',
372
384
  authorizationServerUrl:
373
385
  'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxx',
374
- // ...and point 401s at it for auto-discovery.
375
- resourceMetadataUrl:
376
- 'https://mcp.example.com/.well-known/oauth-protected-resource',
377
386
  // publicMethods defaults to ['initialize', 'tools/list'].
378
387
  },
379
388
  });
380
389
  ```
381
390
 
382
- Both fields are optional. Omitting `resourceMetadataUrl` keeps the bare `Bearer` header, and the `publicMethods` default matches what MCP clients expect for discovery.
391
+ Every field is optional, and the `publicMethods` default matches what MCP clients expect for discovery.
383
392
 
384
393
  ### Supporting clients that connect to the bare origin
385
394
 
@@ -399,7 +408,7 @@ createMcpRouter(mcpServer, {
399
408
  });
400
409
  ```
401
410
 
402
- The discovery endpoint (`/.well-known/oauth-protected-resource`) remains publicly accessible even when `aliases` includes `'/'`.
411
+ The discovery endpoints (both locations above) remain publicly accessible even when `aliases` includes `'/'`. Aliases themselves get no derived location — the metadata `resource` always names the primary `path`.
403
412
 
404
413
  ## Gated Tool Registrar
405
414
 
@@ -492,9 +501,9 @@ Creates a Koa router configured to handle MCP protocol requests.
492
501
  - `auth.cognitoUserPool` — Cognito user pool config (`userPoolId`, `clientId`, `tokenUse`)
493
502
  - `auth.verifyToken` — Custom async token verifier `(token: string) => Promise<unknown>`
494
503
  - `auth.requiredScopes` — Router-level scope guard; returns 403 if any scope is missing
495
- - `auth.resourceServerUrl` + `auth.authorizationServerUrl` — Enable `/.well-known/oauth-protected-resource`; the metadata `resource` is set to `resourceServerUrl + path` so clients following `resource` land on the actual MCP endpoint
504
+ - `auth.resourceServerUrl` + `auth.authorizationServerUrl` — Enable the protected-resource metadata document, served at both `/.well-known/oauth-protected-resource` and the RFC 9728 path-derived `/.well-known/oauth-protected-resource<path>`; the metadata `resource` is set to `resourceServerUrl + path` so clients following `resource` land on the actual MCP endpoint
496
505
  - `auth.publicMethods` — JSON-RPC methods that bypass verification (default `['initialize', 'tools/list']`)
497
- - `auth.resourceMetadataUrl` — Emit RFC 9728 `WWW-Authenticate: Bearer resource_metadata="…"` on 401
506
+ - `auth.resourceMetadataUrl` — URL advertised in the RFC 9728 `WWW-Authenticate: Bearer resource_metadata="…"` header on a 401. Defaults to the location derived from `resourceServerUrl` + `path` (the one this router serves), so the header cannot drift from the routes; set it only to point at a document served elsewhere
498
507
  - `auth.resourceIndicator` — Expected `aud` value(s) (RFC 8707); rejects tokens minted for a different resource. See [Resource indicator validation](#resource-indicator-validation-rfc-8707)
499
508
 
500
509
  **Returns:** `Router` — Koa router instance
package/dist/index.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, {
3
3
  value: 'Module'
4
4
  });
5
+ let _ttoss_auth_core = require("@ttoss/auth-core");
5
6
  let _ttoss_auth_core_amazon_cognito = require("@ttoss/auth-core/amazon-cognito");
6
7
  let _ttoss_http_server = require("@ttoss/http-server");
7
8
  let _ttoss_http_server_auth = require("@ttoss/http-server-auth");
@@ -531,6 +532,21 @@ var createMcpRouter = (server, options = {}) => {
531
532
  let authCheck;
532
533
  if (auth) {
533
534
  const verifyToken = buildVerifyToken(auth);
535
+ const base = auth.resourceServerUrl?.replace(/\/$/, "");
536
+ const resourceUrl = base === void 0 || path === "/" ? base : `${base}${path}`;
537
+ const metadata = resourceUrl !== void 0 && auth.authorizationServerUrl !== void 0 ? {
538
+ document: (0, _ttoss_auth_core.protectedResourceMetadataDocument)({
539
+ resource: resourceUrl,
540
+ authorizationServers: [auth.authorizationServerUrl]
541
+ }),
542
+ paths: (0, _ttoss_auth_core.protectedResourceMetadataPaths)({
543
+ resource: resourceUrl
544
+ }),
545
+ url: (0, _ttoss_auth_core.protectedResourceMetadataUrl)({
546
+ resource: resourceUrl
547
+ })
548
+ } : void 0;
549
+ const resourceMetadataUrl = auth.resourceMetadataUrl ?? metadata?.url;
534
550
  if (auth.publicMethods === void 0) console.warn(`[@ttoss/http-server-mcp] "auth" is configured but "publicMethods" was not set, so it defaults to ${JSON.stringify(DEFAULT_PUBLIC_METHODS)}. This means "tools/list" — the full tool catalogue, including names, descriptions, and input schemas — is served to unauthenticated callers. To require a token for "tools/list" too, set publicMethods: ['initialize']. To keep the current behaviour and silence this warning, set publicMethods: ${JSON.stringify(DEFAULT_PUBLIC_METHODS)} explicitly.`);
535
551
  const publicMethods = new Set(auth.publicMethods ?? DEFAULT_PUBLIC_METHODS);
536
552
  const verify = (0, _ttoss_http_server_auth.authMiddleware)({
@@ -541,7 +557,7 @@ var createMcpRouter = (server, options = {}) => {
541
557
  },
542
558
  requiredScopes: auth.requiredScopes
543
559
  },
544
- resourceMetadataUrl: auth.resourceMetadataUrl
560
+ resourceMetadataUrl
545
561
  });
546
562
  authCheck = async (ctx, next) => {
547
563
  const method = ctx.request.body?.method;
@@ -551,16 +567,9 @@ var createMcpRouter = (server, options = {}) => {
551
567
  }
552
568
  await verify(ctx, next);
553
569
  };
554
- if (auth.resourceServerUrl && auth.authorizationServerUrl) {
555
- const base = auth.resourceServerUrl.replace(/\/$/, "");
556
- const resourceUrl = path === "/" ? base : `${base}${path}`;
557
- router.get("/.well-known/oauth-protected-resource", ctx => {
558
- ctx.body = {
559
- resource: resourceUrl,
560
- authorization_servers: [auth.authorizationServerUrl]
561
- };
562
- });
563
- }
570
+ if (metadata) for (const metadataPath of metadata.paths) router.get(metadataPath, ctx => {
571
+ ctx.body = metadata.document;
572
+ });
564
573
  }
565
574
  const handleWithContext = async (ctx, body) => {
566
575
  const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
package/dist/index.d.cts CHANGED
@@ -900,8 +900,18 @@ interface McpAuthOptions {
900
900
  */
901
901
  publicMethods?: string[];
902
902
  /**
903
- * When set, a `401` carries `WWW-Authenticate: Bearer resource_metadata="…"`
904
- * (RFC 9728) so MCP clients can discover the authorization server.
903
+ * URL advertised in `WWW-Authenticate: Bearer resource_metadata="…"`
904
+ * (RFC 9728) on a `401`, so MCP clients can discover the authorization
905
+ * server.
906
+ *
907
+ * Defaults to the location derived from `resourceServerUrl` + `path` — the
908
+ * same location this router serves the document at — so the header cannot
909
+ * drift from the routes. The default applies only when the document is
910
+ * actually served (both `resourceServerUrl` and `authorizationServerUrl`
911
+ * set); otherwise the header stays a bare `Bearer` rather than advertising a
912
+ * location with no route. Set it only to point at a document served
913
+ * elsewhere; a hand-written value that names a location nothing serves fails
914
+ * discovery silently.
905
915
  */
906
916
  resourceMetadataUrl?: string;
907
917
  /**
package/dist/index.d.mts CHANGED
@@ -901,8 +901,18 @@ interface McpAuthOptions {
901
901
  */
902
902
  publicMethods?: string[];
903
903
  /**
904
- * When set, a `401` carries `WWW-Authenticate: Bearer resource_metadata="…"`
905
- * (RFC 9728) so MCP clients can discover the authorization server.
904
+ * URL advertised in `WWW-Authenticate: Bearer resource_metadata="…"`
905
+ * (RFC 9728) on a `401`, so MCP clients can discover the authorization
906
+ * server.
907
+ *
908
+ * Defaults to the location derived from `resourceServerUrl` + `path` — the
909
+ * same location this router serves the document at — so the header cannot
910
+ * drift from the routes. The default applies only when the document is
911
+ * actually served (both `resourceServerUrl` and `authorizationServerUrl`
912
+ * set); otherwise the header stays a bare `Bearer` rather than advertising a
913
+ * location with no route. Set it only to point at a document served
914
+ * elsewhere; a hand-written value that names a location nothing serves fails
915
+ * discovery silently.
906
916
  */
907
917
  resourceMetadataUrl?: string;
908
918
  /**
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
+ import { protectedResourceMetadataDocument, protectedResourceMetadataPaths, protectedResourceMetadataUrl } from "@ttoss/auth-core";
2
3
  import { CognitoJwtVerifier } from "@ttoss/auth-core/amazon-cognito";
3
4
  import { Router } from "@ttoss/http-server";
4
5
  import { authMiddleware } from "@ttoss/http-server-auth";
@@ -528,6 +529,21 @@ var createMcpRouter = (server, options = {}) => {
528
529
  let authCheck;
529
530
  if (auth) {
530
531
  const verifyToken = buildVerifyToken(auth);
532
+ const base = auth.resourceServerUrl?.replace(/\/$/, "");
533
+ const resourceUrl = base === void 0 || path === "/" ? base : `${base}${path}`;
534
+ const metadata = resourceUrl !== void 0 && auth.authorizationServerUrl !== void 0 ? {
535
+ document: protectedResourceMetadataDocument({
536
+ resource: resourceUrl,
537
+ authorizationServers: [auth.authorizationServerUrl]
538
+ }),
539
+ paths: protectedResourceMetadataPaths({
540
+ resource: resourceUrl
541
+ }),
542
+ url: protectedResourceMetadataUrl({
543
+ resource: resourceUrl
544
+ })
545
+ } : void 0;
546
+ const resourceMetadataUrl = auth.resourceMetadataUrl ?? metadata?.url;
531
547
  if (auth.publicMethods === void 0) console.warn(`[@ttoss/http-server-mcp] "auth" is configured but "publicMethods" was not set, so it defaults to ${JSON.stringify(DEFAULT_PUBLIC_METHODS)}. This means "tools/list" — the full tool catalogue, including names, descriptions, and input schemas — is served to unauthenticated callers. To require a token for "tools/list" too, set publicMethods: ['initialize']. To keep the current behaviour and silence this warning, set publicMethods: ${JSON.stringify(DEFAULT_PUBLIC_METHODS)} explicitly.`);
532
548
  const publicMethods = new Set(auth.publicMethods ?? DEFAULT_PUBLIC_METHODS);
533
549
  const verify = authMiddleware({
@@ -538,7 +554,7 @@ var createMcpRouter = (server, options = {}) => {
538
554
  },
539
555
  requiredScopes: auth.requiredScopes
540
556
  },
541
- resourceMetadataUrl: auth.resourceMetadataUrl
557
+ resourceMetadataUrl
542
558
  });
543
559
  authCheck = async (ctx, next) => {
544
560
  const method = ctx.request.body?.method;
@@ -548,16 +564,9 @@ var createMcpRouter = (server, options = {}) => {
548
564
  }
549
565
  await verify(ctx, next);
550
566
  };
551
- if (auth.resourceServerUrl && auth.authorizationServerUrl) {
552
- const base = auth.resourceServerUrl.replace(/\/$/, "");
553
- const resourceUrl = path === "/" ? base : `${base}${path}`;
554
- router.get("/.well-known/oauth-protected-resource", ctx => {
555
- ctx.body = {
556
- resource: resourceUrl,
557
- authorization_servers: [auth.authorizationServerUrl]
558
- };
559
- });
560
- }
567
+ if (metadata) for (const metadataPath of metadata.paths) router.get(metadataPath, ctx => {
568
+ ctx.body = metadata.document;
569
+ });
561
570
  }
562
571
  const handleWithContext = async (ctx, body) => {
563
572
  const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttoss/http-server-mcp",
3
- "version": "0.25.2",
3
+ "version": "0.26.1",
4
4
  "description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
5
5
  "keywords": [
6
6
  "ai",
@@ -36,16 +36,16 @@
36
36
  "@modelcontextprotocol/node": "^2.0.0",
37
37
  "@modelcontextprotocol/server": "^2.0.0",
38
38
  "zod": "^4.4.3",
39
- "@ttoss/http-server-auth": "^0.6.2",
40
- "@ttoss/http-server": "^0.8.0",
41
- "@ttoss/auth-core": "^0.13.1"
39
+ "@ttoss/auth-core": "^0.14.1",
40
+ "@ttoss/http-server-auth": "^0.6.4",
41
+ "@ttoss/http-server": "^0.8.1"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/koa": "^3.0.3",
45
45
  "jest": "^30.4.2",
46
46
  "supertest": "^7.2.2",
47
47
  "tsdown": "^0.22.2",
48
- "@ttoss/config": "^1.37.17"
48
+ "@ttoss/config": "^1.38.0"
49
49
  },
50
50
  "publishConfig": {
51
51
  "access": "public",