@ttoss/http-server-mcp 0.26.0 → 0.26.2

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. When the document is not served, the header stays a bare `Bearer` rather than naming a location with no route. The field exists for the one configuration where this router deliberately does not serve the document — an `oauthServer()` in the same deployment already answers that path, so this router is mounted without `resourceServerUrl`/`authorizationServerUrl` to avoid two routers on one path, and has nothing to derive from. Compute the value with `protectedResourceMetadataUrl` from [`@ttoss/auth-core`](https://ttoss.dev/docs/modules/packages/auth-core) rather than typing it, so both halves apply the same §3.1 rule.
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 when a separate `oauthServer()` serves the document and this router is mounted without `resourceServerUrl`/`authorizationServerUrl`
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,37 @@ 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.
913
+ *
914
+ * **Leave it unset when this router serves the document.** It exists for the
915
+ * one configuration where that is deliberately not the case: an
916
+ * authorization server in the same deployment (`oauthServer({ resource })`)
917
+ * already answers `/.well-known/oauth-protected-resource`, so mounting this
918
+ * router with `resourceServerUrl` + `authorizationServerUrl` too would put
919
+ * two routers on one path. Omit both there and set this instead — without
920
+ * it the `401` is a bare `Bearer`, which never starts a client's discovery.
921
+ *
922
+ * Derive the value rather than typing it: `protectedResourceMetadataUrl`
923
+ * from `@ttoss/auth-core` applies the same RFC 9728 §3.1 rule the serving
924
+ * side does. A hand-written value that names a location nothing serves
925
+ * fails discovery silently.
926
+ *
927
+ * @example
928
+ * ```typescript
929
+ * // Only when oauthServer (not this router) serves the document.
930
+ * resourceMetadataUrl: protectedResourceMetadataUrl({
931
+ * resource: 'https://mcp.example.com/mcp',
932
+ * }),
933
+ * ```
905
934
  */
906
935
  resourceMetadataUrl?: string;
907
936
  /**
package/dist/index.d.mts CHANGED
@@ -901,8 +901,37 @@ 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.
914
+ *
915
+ * **Leave it unset when this router serves the document.** It exists for the
916
+ * one configuration where that is deliberately not the case: an
917
+ * authorization server in the same deployment (`oauthServer({ resource })`)
918
+ * already answers `/.well-known/oauth-protected-resource`, so mounting this
919
+ * router with `resourceServerUrl` + `authorizationServerUrl` too would put
920
+ * two routers on one path. Omit both there and set this instead — without
921
+ * it the `401` is a bare `Bearer`, which never starts a client's discovery.
922
+ *
923
+ * Derive the value rather than typing it: `protectedResourceMetadataUrl`
924
+ * from `@ttoss/auth-core` applies the same RFC 9728 §3.1 rule the serving
925
+ * side does. A hand-written value that names a location nothing serves
926
+ * fails discovery silently.
927
+ *
928
+ * @example
929
+ * ```typescript
930
+ * // Only when oauthServer (not this router) serves the document.
931
+ * resourceMetadataUrl: protectedResourceMetadataUrl({
932
+ * resource: 'https://mcp.example.com/mcp',
933
+ * }),
934
+ * ```
906
935
  */
907
936
  resourceMetadataUrl?: string;
908
937
  /**
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.26.0",
3
+ "version": "0.26.2",
4
4
  "description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
5
5
  "keywords": [
6
6
  "ai",
@@ -36,9 +36,9 @@
36
36
  "@modelcontextprotocol/node": "^2.0.0",
37
37
  "@modelcontextprotocol/server": "^2.0.0",
38
38
  "zod": "^4.4.3",
39
- "@ttoss/http-server": "^0.8.1",
40
- "@ttoss/http-server-auth": "^0.6.3",
41
- "@ttoss/auth-core": "^0.14.0"
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",