@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 +18 -9
- package/dist/index.cjs +20 -11
- package/dist/index.d.cts +31 -2
- package/dist/index.d.mts +31 -2
- package/dist/index.mjs +20 -11
- package/package.json +4 -4
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`** —
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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` —
|
|
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
|
|
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 (
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
*
|
|
904
|
-
* (RFC 9728) so MCP clients can discover the authorization
|
|
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
|
-
*
|
|
905
|
-
* (RFC 9728) so MCP clients can discover the authorization
|
|
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
|
|
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 (
|
|
552
|
-
|
|
553
|
-
|
|
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.
|
|
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/
|
|
40
|
-
"@ttoss/http-server-auth": "^0.6.
|
|
41
|
-
"@ttoss/
|
|
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",
|