@ttoss/http-server-mcp 0.26.2 → 0.27.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/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', 'server/discover']` — the lifecycle handshake of each protocol era, which the spec sanctions so a client can complete it 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', 'server/discover'].
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', 'server/discover']`)
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', 'server/discover']`: the lifecycle handshake is reachable without a token, nothing else is. There are two entries because the exemption is about _the handshake_, not the method name — `initialize` is the 2025-era handshake and `server/discover` its `2026-07-28` replacement, since that revision removed `initialize` outright. Naming only one leaves the other era unable to negotiate at all.
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
+ **Whether the handshake should be public at all is a separate question.** `publicMethods: []` closes both eras, and the authorization flow still works — RFC 9728 discovery is driven by the challenge itself, so a client authenticates from its very first request instead of one step later.
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,24 @@ 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 handshakes reachable before a client authenticates.
331
+ *
332
+ * One entry per protocol era, because the exemption the MCP authorization spec
333
+ * sanctions is about *the handshake* — a client has to complete it before it
334
+ * can discover the authorization server — not about a method name:
335
+ *
336
+ * - `initialize` is the 2025-era handshake.
337
+ * - `server/discover` is its `2026-07-28` replacement. That revision removed
338
+ * `initialize` entirely, so naming only `initialize` left the modern era
339
+ * with no public method at all — the asymmetry ttoss/ttoss#1222 fixed.
340
+ *
341
+ * A future revision that renames the handshake again needs a matching entry
342
+ * here; nothing detects that automatically.
343
+ *
344
+ * `tools/list` is deliberately absent — see {@link McpAuthOptions.publicMethods}.
345
+ */
346
+ var DEFAULT_PUBLIC_METHODS = ["initialize", "server/discover"];
331
347
  /**
332
348
  * Asserts that a verified token's `aud` claim includes at least one of the
333
349
  * expected resource indicator values. `aud` may be a single string or an
@@ -547,7 +563,6 @@ var createMcpRouter = (server, options = {}) => {
547
563
  })
548
564
  } : void 0;
549
565
  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
566
  const publicMethods = new Set(auth.publicMethods ?? DEFAULT_PUBLIC_METHODS);
552
567
  const verify = (0, _ttoss_http_server_auth.authMiddleware)({
553
568
  strategies: ["oauth"],
package/dist/index.d.cts CHANGED
@@ -892,11 +892,35 @@ 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
+ * The lifecycle handshake is public because the MCP authorization spec
897
+ * sanctions it: a client completes it before it can discover the
898
+ * authorization server. That means one entry per protocol era —
899
+ * `initialize` on 2025, `server/discover` on `2026-07-28`, which removed
900
+ * `initialize` — so neither era is left unable to negotiate. Nothing else
901
+ * is public, so a server with `auth` configured serves no tool metadata to
902
+ * an unauthenticated caller.
903
+ *
904
+ * Opening `tools/list` is a deliberate choice, not a default. It serves the
905
+ * full tool catalogue — every name, description, and input schema — to
906
+ * anyone who can reach the endpoint, which for an OpenAPI-derived server is
907
+ * a map of the whole underlying API. It buys an OAuth client nothing: the
908
+ * `401` and its RFC 9728 `WWW-Authenticate` challenge are what start the
909
+ * authorization flow, and a client that lists tools anonymously still
910
+ * cannot call one. Set it only to serve callers that will never
911
+ * authenticate.
912
+ *
913
+ * @default ['initialize', 'server/discover']
914
+ *
915
+ * @example
916
+ * ```typescript
917
+ * // Restore unauthenticated tool discovery.
918
+ * publicMethods: ['initialize', 'tools/list'],
919
+ *
920
+ * // Require a token for the handshake too, so an OAuth client
921
+ * // self-discovers from its very first request.
922
+ * publicMethods: [],
923
+ * ```
900
924
  */
901
925
  publicMethods?: string[];
902
926
  /**
@@ -1114,9 +1138,10 @@ interface McpRouterOptions {
1114
1138
  * OAuth / JWT authentication configuration for the MCP endpoint.
1115
1139
  *
1116
1140
  * 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
1141
+ * `Authorization` header — except for `publicMethods` (by default the
1142
+ * lifecycle handshake of each protocol era: `initialize` and
1143
+ * `server/discover`), which bypass verification so a client can complete
1144
+ * the handshake before authenticating. Invalid or missing tokens
1120
1145
  * receive a `401` response with `WWW-Authenticate: Bearer` (or
1121
1146
  * `Bearer resource_metadata="..."` when `resourceMetadataUrl` is set, per
1122
1147
  * RFC 9728). Tokens that fail a `requiredScopes` check receive `403`.
package/dist/index.d.mts CHANGED
@@ -893,11 +893,35 @@ 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
+ * The lifecycle handshake is public because the MCP authorization spec
898
+ * sanctions it: a client completes it before it can discover the
899
+ * authorization server. That means one entry per protocol era —
900
+ * `initialize` on 2025, `server/discover` on `2026-07-28`, which removed
901
+ * `initialize` — so neither era is left unable to negotiate. Nothing else
902
+ * is public, so a server with `auth` configured serves no tool metadata to
903
+ * an unauthenticated caller.
904
+ *
905
+ * Opening `tools/list` is a deliberate choice, not a default. It serves the
906
+ * full tool catalogue — every name, description, and input schema — to
907
+ * anyone who can reach the endpoint, which for an OpenAPI-derived server is
908
+ * a map of the whole underlying API. It buys an OAuth client nothing: the
909
+ * `401` and its RFC 9728 `WWW-Authenticate` challenge are what start the
910
+ * authorization flow, and a client that lists tools anonymously still
911
+ * cannot call one. Set it only to serve callers that will never
912
+ * authenticate.
913
+ *
914
+ * @default ['initialize', 'server/discover']
915
+ *
916
+ * @example
917
+ * ```typescript
918
+ * // Restore unauthenticated tool discovery.
919
+ * publicMethods: ['initialize', 'tools/list'],
920
+ *
921
+ * // Require a token for the handshake too, so an OAuth client
922
+ * // self-discovers from its very first request.
923
+ * publicMethods: [],
924
+ * ```
901
925
  */
902
926
  publicMethods?: string[];
903
927
  /**
@@ -1115,9 +1139,10 @@ interface McpRouterOptions {
1115
1139
  * OAuth / JWT authentication configuration for the MCP endpoint.
1116
1140
  *
1117
1141
  * 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
1142
+ * `Authorization` header — except for `publicMethods` (by default the
1143
+ * lifecycle handshake of each protocol era: `initialize` and
1144
+ * `server/discover`), which bypass verification so a client can complete
1145
+ * the handshake before authenticating. Invalid or missing tokens
1121
1146
  * receive a `401` response with `WWW-Authenticate: Bearer` (or
1122
1147
  * `Bearer resource_metadata="..."` when `resourceMetadataUrl` is set, per
1123
1148
  * RFC 9728). Tokens that fail a `requiredScopes` check receive `403`.
package/dist/index.mjs CHANGED
@@ -323,8 +323,24 @@ 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 handshakes reachable before a client authenticates.
328
+ *
329
+ * One entry per protocol era, because the exemption the MCP authorization spec
330
+ * sanctions is about *the handshake* — a client has to complete it before it
331
+ * can discover the authorization server — not about a method name:
332
+ *
333
+ * - `initialize` is the 2025-era handshake.
334
+ * - `server/discover` is its `2026-07-28` replacement. That revision removed
335
+ * `initialize` entirely, so naming only `initialize` left the modern era
336
+ * with no public method at all — the asymmetry ttoss/ttoss#1222 fixed.
337
+ *
338
+ * A future revision that renames the handshake again needs a matching entry
339
+ * here; nothing detects that automatically.
340
+ *
341
+ * `tools/list` is deliberately absent — see {@link McpAuthOptions.publicMethods}.
342
+ */
343
+ var DEFAULT_PUBLIC_METHODS = ["initialize", "server/discover"];
328
344
  /**
329
345
  * Asserts that a verified token's `aud` claim includes at least one of the
330
346
  * expected resource indicator values. `aud` may be a single string or an
@@ -544,7 +560,6 @@ var createMcpRouter = (server, options = {}) => {
544
560
  })
545
561
  } : void 0;
546
562
  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
563
  const publicMethods = new Set(auth.publicMethods ?? DEFAULT_PUBLIC_METHODS);
549
564
  const verify = authMiddleware({
550
565
  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.1",
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",