@ttoss/http-server-mcp 0.26.2 → 0.27.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/MIGRATIONS.md ADDED
@@ -0,0 +1,38 @@
1
+ # Migrations
2
+
3
+ ## `tools/list` is no longer public by default
4
+
5
+ `auth.publicMethods` now defaults to `['initialize']` instead of
6
+ `['initialize', 'tools/list']`. A server with `auth` configured no longer
7
+ serves its tool catalogue to unauthenticated callers.
8
+
9
+ ```diff
10
+ createMcpRouter(mcpServer, {
11
+ auth: {
12
+ cognitoUserPool: { userPoolId: '...', clientId: '...' },
13
+ + // Only needed if unauthenticated callers must keep listing tools.
14
+ + publicMethods: ['initialize', 'tools/list'],
15
+ },
16
+ });
17
+ ```
18
+
19
+ **What you will observe if you miss this.** An unauthenticated `tools/list`
20
+ returns `401` with `WWW-Authenticate: Bearer resource_metadata="…"` instead of
21
+ `200` and the catalogue. It fails on the first such request rather than on
22
+ particular inputs, so a smoke test against a deployed server surfaces it
23
+ immediately.
24
+
25
+ **Who is affected.** Only consumers that configure `auth` and never set
26
+ `publicMethods`. Anyone already passing `publicMethods` — including
27
+ `publicMethods: []` — is unaffected, and so is any server without `auth`.
28
+
29
+ **Who should not restore the old value.** OAuth clients do not need it. The
30
+ `401` and its RFC 9728 challenge are what start the authorization flow, and a
31
+ client that lists tools anonymously still cannot call one; the flow reaches the
32
+ authorization redirect identically with `tools/list` open or closed. Restore it
33
+ only to serve callers that will never authenticate, and note what that exposes:
34
+ every tool name, description, and input schema, which for an OpenAPI-derived
35
+ server is a map of the whole underlying API.
36
+
37
+ The one-time startup warning that pre-announced this change is gone, since the
38
+ default it warned about is now the secure one.
package/README.md CHANGED
@@ -371,7 +371,7 @@ The `WWW-Authenticate: Bearer resource_metadata="…"` header is how MCP clients
371
371
 
372
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:
373
373
 
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.
374
+ - **`publicMethods`** — JSON-RPC methods that bypass verification, read from the request body's `method` field. Defaults to `['initialize']`, which the spec sanctions so a client can complete the lifecycle handshake before it can discover the authorization server. Pass `[]` to require a token for every method, or a custom list to change the exempt set. Adding `tools/list` serves the full tool catalogue — every tool name, description, and input schema — to unauthenticated callers; see [`publicMethods` and OAuth clients](#publicmethods-and-oauth-clients) before doing so.
375
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.
376
376
 
377
377
  ```typescript
@@ -383,12 +383,12 @@ createMcpRouter(mcpServer, {
383
383
  resourceServerUrl: 'https://mcp.example.com',
384
384
  authorizationServerUrl:
385
385
  'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxx',
386
- // publicMethods defaults to ['initialize', 'tools/list'].
386
+ // publicMethods defaults to ['initialize'].
387
387
  },
388
388
  });
389
389
  ```
390
390
 
391
- Every field is optional, and the `publicMethods` default matches what MCP clients expect for discovery.
391
+ Every field is optional.
392
392
 
393
393
  ### Supporting clients that connect to the bare origin
394
394
 
@@ -502,7 +502,7 @@ Creates a Koa router configured to handle MCP protocol requests.
502
502
  - `auth.verifyToken` — Custom async token verifier `(token: string) => Promise<unknown>`
503
503
  - `auth.requiredScopes` — Router-level scope guard; returns 403 if any scope is missing
504
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
505
- - `auth.publicMethods` — JSON-RPC methods that bypass verification (default `['initialize', 'tools/list']`)
505
+ - `auth.publicMethods` — JSON-RPC methods that bypass verification (default `['initialize']`)
506
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`
507
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)
508
508
 
@@ -873,14 +873,19 @@ expect(call.status).toBe(200);
873
873
 
874
874
  ### `publicMethods` and OAuth clients
875
875
 
876
- `auth.publicMethods` defaults to `['initialize', 'tools/list']`, which lets clients discover the server before they have a token. This default has an important effect on OAuth-aware clients (e.g. a Claude connector):
876
+ `auth.publicMethods` defaults to `['initialize']`: the handshake is reachable without a token, nothing else is.
877
877
 
878
- - With the default, `initialize` returns `200` unauthenticated, so an OAuth client concludes the server is public and never starts the OAuth flow — while `notifications/initialized` still returns `401`, breaking the handshake. The visible symptom is "connected, no tools available, no sign-in prompt".
879
- - Setting `publicMethods: []` makes `initialize` return `401` + `WWW-Authenticate`, which triggers the client's OAuth discovery and PKCE flow.
878
+ **Adding `tools/list` is a deliberate exposure.** It serves the full tool catalogue — every name, description, and input schema — to anyone who can reach the endpoint. Those leak internal resource names and domain vocabulary, and for an OpenAPI-derived server the schemas mirror the REST surface, so the catalogue becomes an unauthenticated map of the whole API including operations the caller could never invoke. It buys an OAuth client nothing, because the `401` is what starts the authorization flow and a client that lists tools anonymously still cannot call one. Open it only to serve callers that will never authenticate:
880
879
 
881
- Use the default when token auth is handled outside the OAuth flow (Cognito, API keys, Bearer tokens). Set `publicMethods: []` only when you want OAuth clients to self-discover and authenticate before anything else.
880
+ ```typescript
881
+ publicMethods: ['initialize', 'tools/list'],
882
+ ```
883
+
884
+ **Whether `initialize` should be public depends on how tokens are issued.** Keep the default when token auth is handled outside the OAuth flow (Cognito, API keys, Bearer tokens minted elsewhere). Set `publicMethods: []` when you want OAuth clients to authenticate before anything else — `initialize` then returns `401` + `WWW-Authenticate` on the very first request, so discovery starts there rather than one step later.
882
885
 
883
- Leaving `publicMethods` unset also exposes the full tool catalogue (`tools/list`) to unauthenticated callers — a tool's name, description, and input schema can leak internal resource names and, for an OpenAPI-derived server, the shape of the whole underlying API. A one-time warning is logged when `auth` is configured without an explicit `publicMethods`, naming the exposure and how to close it. This default is a candidate to tighten to `['initialize']` in a future major; track the decision in [ttoss/ttoss#1176](https://github.com/ttoss/ttoss/issues/1176).
886
+ Either way the authorization flow works. Driving the official MCP client SDK against this router shows the RFC 9728 challenge resolving to the protected-resource document, the authorization server, and the redirect, whether the `401` arrives on `initialize` (`publicMethods: []`) or on the request after it (the default). `tests/unit/tests/oauth-client-flow.test.ts` asserts it.
887
+
888
+ On the `2026-07-28` revision `initialize` does not exist — discovery is `server/discover`, which is not in the default set and therefore always requires a token. That is deliberate: RFC 9728 discovery is driven by the challenge itself.
884
889
 
885
890
  ## AWS Lambda Deployment
886
891
 
@@ -931,6 +936,12 @@ Keep the following in mind:
931
936
 
932
937
  > This differs from [`awslabs/run-model-context-protocol-servers-with-aws-lambda`](https://github.com/awslabs/run-model-context-protocol-servers-with-aws-lambda), which wraps **stdio**-based MCP servers into Lambda by spawning a child process per invocation. `@ttoss/http-server-mcp` already speaks Streamable HTTP, so that wrapper is unnecessary — you deploy it like any other HTTP handler.
933
938
 
939
+ ## Migrations
940
+
941
+ Breaking changes and what they require of consumers are listed in
942
+ [MIGRATIONS.md](https://github.com/ttoss/ttoss/blob/main/packages/http-server-mcp/MIGRATIONS.md),
943
+ newest first.
944
+
934
945
  ## Related Packages
935
946
 
936
947
  - [@ttoss/http-server](https://ttoss.dev/docs/modules/packages/http-server) - HTTP server foundation
package/dist/index.cjs CHANGED
@@ -326,8 +326,14 @@ var createGatedToolRegistrar = ({
326
326
 
327
327
  //#endregion
328
328
  //#region src/index.ts
329
- /** MCP lifecycle/discovery methods reachable before a client authenticates. */
330
- var DEFAULT_PUBLIC_METHODS = ["initialize", "tools/list"];
329
+ /**
330
+ * MCP lifecycle methods reachable before a client authenticates.
331
+ *
332
+ * Only `initialize`, which the MCP authorization spec sanctions: a client has
333
+ * to complete the handshake before it can discover the authorization server.
334
+ * `tools/list` is not here — see {@link McpAuthOptions.publicMethods}.
335
+ */
336
+ var DEFAULT_PUBLIC_METHODS = ["initialize"];
331
337
  /**
332
338
  * Asserts that a verified token's `aud` claim includes at least one of the
333
339
  * expected resource indicator values. `aud` may be a single string or an
@@ -547,7 +553,6 @@ var createMcpRouter = (server, options = {}) => {
547
553
  })
548
554
  } : void 0;
549
555
  const resourceMetadataUrl = auth.resourceMetadataUrl ?? metadata?.url;
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.`);
551
556
  const publicMethods = new Set(auth.publicMethods ?? DEFAULT_PUBLIC_METHODS);
552
557
  const verify = (0, _ttoss_http_server_auth.authMiddleware)({
553
558
  strategies: ["oauth"],
package/dist/index.d.cts CHANGED
@@ -892,11 +892,32 @@ interface McpAuthOptions {
892
892
  requiredScopes?: string[];
893
893
  /**
894
894
  * JSON-RPC methods (read from `body.method`) that bypass verification.
895
- * Leaving this unset serves `tools/list` — the full tool catalogue —
896
- * to unauthenticated callers, and logs a one-time warning explaining how
897
- * to close it. Set explicitly (even to the same default) to silence the
898
- * warning; set to `['initialize']` to require a token for `tools/list` too.
899
- * @default ['initialize', 'tools/list']
895
+ *
896
+ * `initialize` is public because the MCP authorization spec sanctions it: a
897
+ * client completes the lifecycle handshake before it can discover the
898
+ * authorization server. Nothing else is, so a server with `auth` configured
899
+ * serves no tool metadata to an unauthenticated caller.
900
+ *
901
+ * Opening `tools/list` is a deliberate choice, not a default. It serves the
902
+ * full tool catalogue — every name, description, and input schema — to
903
+ * anyone who can reach the endpoint, which for an OpenAPI-derived server is
904
+ * a map of the whole underlying API. It buys an OAuth client nothing: the
905
+ * `401` and its RFC 9728 `WWW-Authenticate` challenge are what start the
906
+ * authorization flow, and a client that lists tools anonymously still
907
+ * cannot call one. Set it only to serve callers that will never
908
+ * authenticate.
909
+ *
910
+ * @default ['initialize']
911
+ *
912
+ * @example
913
+ * ```typescript
914
+ * // Restore unauthenticated tool discovery.
915
+ * publicMethods: ['initialize', 'tools/list'],
916
+ *
917
+ * // Require a token for the handshake too, so an OAuth client
918
+ * // self-discovers from its very first request.
919
+ * publicMethods: [],
920
+ * ```
900
921
  */
901
922
  publicMethods?: string[];
902
923
  /**
@@ -1114,9 +1135,9 @@ interface McpRouterOptions {
1114
1135
  * OAuth / JWT authentication configuration for the MCP endpoint.
1115
1136
  *
1116
1137
  * When set, incoming MCP requests must include a valid Bearer token in the
1117
- * `Authorization` header — except for `publicMethods` (by default
1118
- * `initialize` and `tools/list`), which bypass verification so clients can
1119
- * discover the server before authenticating. Invalid or missing tokens
1138
+ * `Authorization` header — except for `publicMethods` (by default just
1139
+ * `initialize`), which bypasses verification so a client can complete the
1140
+ * lifecycle handshake before authenticating. Invalid or missing tokens
1120
1141
  * receive a `401` response with `WWW-Authenticate: Bearer` (or
1121
1142
  * `Bearer resource_metadata="..."` when `resourceMetadataUrl` is set, per
1122
1143
  * RFC 9728). Tokens that fail a `requiredScopes` check receive `403`.
package/dist/index.d.mts CHANGED
@@ -893,11 +893,32 @@ interface McpAuthOptions {
893
893
  requiredScopes?: string[];
894
894
  /**
895
895
  * JSON-RPC methods (read from `body.method`) that bypass verification.
896
- * Leaving this unset serves `tools/list` — the full tool catalogue —
897
- * to unauthenticated callers, and logs a one-time warning explaining how
898
- * to close it. Set explicitly (even to the same default) to silence the
899
- * warning; set to `['initialize']` to require a token for `tools/list` too.
900
- * @default ['initialize', 'tools/list']
896
+ *
897
+ * `initialize` is public because the MCP authorization spec sanctions it: a
898
+ * client completes the lifecycle handshake before it can discover the
899
+ * authorization server. Nothing else is, so a server with `auth` configured
900
+ * serves no tool metadata to an unauthenticated caller.
901
+ *
902
+ * Opening `tools/list` is a deliberate choice, not a default. It serves the
903
+ * full tool catalogue — every name, description, and input schema — to
904
+ * anyone who can reach the endpoint, which for an OpenAPI-derived server is
905
+ * a map of the whole underlying API. It buys an OAuth client nothing: the
906
+ * `401` and its RFC 9728 `WWW-Authenticate` challenge are what start the
907
+ * authorization flow, and a client that lists tools anonymously still
908
+ * cannot call one. Set it only to serve callers that will never
909
+ * authenticate.
910
+ *
911
+ * @default ['initialize']
912
+ *
913
+ * @example
914
+ * ```typescript
915
+ * // Restore unauthenticated tool discovery.
916
+ * publicMethods: ['initialize', 'tools/list'],
917
+ *
918
+ * // Require a token for the handshake too, so an OAuth client
919
+ * // self-discovers from its very first request.
920
+ * publicMethods: [],
921
+ * ```
901
922
  */
902
923
  publicMethods?: string[];
903
924
  /**
@@ -1115,9 +1136,9 @@ interface McpRouterOptions {
1115
1136
  * OAuth / JWT authentication configuration for the MCP endpoint.
1116
1137
  *
1117
1138
  * When set, incoming MCP requests must include a valid Bearer token in the
1118
- * `Authorization` header — except for `publicMethods` (by default
1119
- * `initialize` and `tools/list`), which bypass verification so clients can
1120
- * discover the server before authenticating. Invalid or missing tokens
1139
+ * `Authorization` header — except for `publicMethods` (by default just
1140
+ * `initialize`), which bypasses verification so a client can complete the
1141
+ * lifecycle handshake before authenticating. Invalid or missing tokens
1121
1142
  * receive a `401` response with `WWW-Authenticate: Bearer` (or
1122
1143
  * `Bearer resource_metadata="..."` when `resourceMetadataUrl` is set, per
1123
1144
  * RFC 9728). Tokens that fail a `requiredScopes` check receive `403`.
package/dist/index.mjs CHANGED
@@ -323,8 +323,14 @@ var createGatedToolRegistrar = ({
323
323
 
324
324
  //#endregion
325
325
  //#region src/index.ts
326
- /** MCP lifecycle/discovery methods reachable before a client authenticates. */
327
- var DEFAULT_PUBLIC_METHODS = ["initialize", "tools/list"];
326
+ /**
327
+ * MCP lifecycle methods reachable before a client authenticates.
328
+ *
329
+ * Only `initialize`, which the MCP authorization spec sanctions: a client has
330
+ * to complete the handshake before it can discover the authorization server.
331
+ * `tools/list` is not here — see {@link McpAuthOptions.publicMethods}.
332
+ */
333
+ var DEFAULT_PUBLIC_METHODS = ["initialize"];
328
334
  /**
329
335
  * Asserts that a verified token's `aud` claim includes at least one of the
330
336
  * expected resource indicator values. `aud` may be a single string or an
@@ -544,7 +550,6 @@ var createMcpRouter = (server, options = {}) => {
544
550
  })
545
551
  } : void 0;
546
552
  const resourceMetadataUrl = auth.resourceMetadataUrl ?? metadata?.url;
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.`);
548
553
  const publicMethods = new Set(auth.publicMethods ?? DEFAULT_PUBLIC_METHODS);
549
554
  const verify = authMiddleware({
550
555
  strategies: ["oauth"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttoss/http-server-mcp",
3
- "version": "0.26.2",
3
+ "version": "0.27.0",
4
4
  "description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
5
5
  "keywords": [
6
6
  "ai",
@@ -30,6 +30,7 @@
30
30
  }
31
31
  },
32
32
  "files": [
33
+ "MIGRATIONS.md",
33
34
  "dist"
34
35
  ],
35
36
  "dependencies": {
@@ -37,10 +38,11 @@
37
38
  "@modelcontextprotocol/server": "^2.0.0",
38
39
  "zod": "^4.4.3",
39
40
  "@ttoss/auth-core": "^0.14.1",
40
- "@ttoss/http-server-auth": "^0.6.4",
41
- "@ttoss/http-server": "^0.8.1"
41
+ "@ttoss/http-server": "^0.8.1",
42
+ "@ttoss/http-server-auth": "^0.6.4"
42
43
  },
43
44
  "devDependencies": {
45
+ "@modelcontextprotocol/client": "^2.0.0",
44
46
  "@types/koa": "^3.0.3",
45
47
  "jest": "^30.4.2",
46
48
  "supertest": "^7.2.2",