@ttoss/http-server-mcp 0.16.0 → 0.16.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
@@ -340,74 +340,9 @@ createMcpRouter(mcpServer, {
340
340
 
341
341
  Both fields are optional. Omitting `resourceMetadataUrl` keeps the bare `Bearer` header, and the `publicMethods` default matches what MCP clients expect for discovery.
342
342
 
343
- ## OAuth 2.1 Authorization Server
343
+ ## Issuing tokens for MCP clients
344
344
 
345
- The `auth` option above covers the **resource-server** half of MCP authorization — it verifies tokens issued by an external authorization server (Cognito, Auth0, …). When you want your own first-party server to _issue_ the tokens, `createMcpAuthServer` provides the **authorization-server** half: the `/authorize`, `/token`, `/register`, and discovery endpoints an MCP client (Claude, Cursor, VS Code) auto-discovers and runs the full OAuth 2.1 flow against.
346
-
347
- ttoss owns only the protocol mechanics (PKCE, discovery metadata, code exchange, dynamic client registration). Your app keeps its own user model, token signing, and login/consent UI through pluggable hooks — the user model never leaves your app.
348
-
349
- ```typescript
350
- import { App, bodyParser } from '@ttoss/http-server';
351
- import { createMcpAuthServer } from '@ttoss/http-server-mcp';
352
-
353
- const authServer = createMcpAuthServer({
354
- issuer: 'https://api.example.com',
355
- clientStore, // register/lookup dynamic clients
356
- authCodeStore, // persist short-lived codes + PKCE challenge
357
- // App-owned token minting — ttoss never sees your signing keys
358
- issueTokens: async ({ subject, scopes }) => ({
359
- accessToken: signJwt({ sub: subject, scope: scopes.join(' ') }),
360
- refreshToken: createRefreshToken(subject),
361
- expiresIn: 3600,
362
- }),
363
- // App-owned login/consent — show your own UI, then approve
364
- onAuthorize: async ({ ctx, request }) => {
365
- const session = await getSession(ctx);
366
- if (!session) {
367
- ctx.redirect(`/login?return_to=${encodeURIComponent(ctx.url)}`);
368
- return { approved: false };
369
- }
370
- return { approved: true, subject: session.userId, scopes: request.scopes };
371
- },
372
- // App-owned refresh validation — required to enable the refresh_token grant
373
- onRefreshToken: async ({ refreshToken }) => {
374
- const claims = await verifyRefreshToken(refreshToken);
375
- return claims ? { subject: claims.sub, scopes: claims.scopes } : undefined;
376
- },
377
- scopesSupported: ['mcp:access'],
378
- });
379
-
380
- const app = new App();
381
- app.use(bodyParser());
382
- app.use(authServer.routes());
383
- ```
384
-
385
- This mounts:
386
-
387
- | Endpoint | Spec | Purpose |
388
- | ----------------------------------------- | --------- | ---------------------------------------------------- |
389
- | `/.well-known/oauth-authorization-server` | RFC 8414 | Authorization server metadata discovery |
390
- | `/.well-known/oauth-protected-resource` | RFC 9728 | Protected-resource metadata (when `resource` is set) |
391
- | `/authorize` | OAuth 2.1 | Authorization endpoint — **PKCE (S256) required** |
392
- | `/token` | OAuth 2.1 | `authorization_code` + `refresh_token` grants |
393
- | `/register` | RFC 7591 | Dynamic Client Registration |
394
-
395
- The full flow an MCP client runs: it fetches the discovery document, self-registers at `/register`, opens `/authorize` (your `onAuthorize` handles login/consent), exchanges the returned code at `/token` with its PKCE verifier, and uses the access token against your `createMcpRouter` endpoint. Pair this with `createMcpRouter({ auth: { verifyToken } })` so the same server both issues and verifies tokens.
396
-
397
- The pluggable stores let you keep persistence in your own datastore:
398
-
399
- ```typescript
400
- const clientStore: ClientStore = {
401
- get: (clientId) => db.clients.findById(clientId),
402
- register: (client) => db.clients.put(client),
403
- };
404
-
405
- const authCodeStore: AuthCodeStore = {
406
- save: (code) => db.codes.put(code), // set a TTL matching authorizationCodeTtl
407
- get: (code) => db.codes.findById(code),
408
- delete: (code) => db.codes.remove(code),
409
- };
410
- ```
345
+ The `auth` option above covers the **resource-server** half of MCP authorization — it verifies tokens issued by an external authorization server (Cognito, Auth0, …). To make your own first-party server _issue_ the tokens an MCP client runs the full OAuth flow against, add the [`@ttoss/http-server-auth`](https://ttoss.dev/docs/modules/packages/http-server-auth) plugin's `oauthServer()` and pair it with `createMcpRouter({ auth: { verifyToken } })` so one deployment both issues and verifies tokens. See the [OAuth Authorization Server](https://ttoss.dev/docs/engineering/guidelines/oauth-authorization-server) guideline.
411
346
 
412
347
  ## API Reference
413
348
 
@@ -433,25 +368,6 @@ Creates a Koa router configured to handle MCP protocol requests.
433
368
 
434
369
  **Returns:** `Router` — Koa router instance
435
370
 
436
- ### `createMcpAuthServer(options)`
437
-
438
- Creates an OAuth 2.1 Authorization Server (`/authorize`, `/token`, `/register`, and discovery metadata) for MCP clients. See [OAuth 2.1 Authorization Server](#oauth-21-authorization-server).
439
-
440
- **Parameters (`McpAuthServerOptions`):**
441
-
442
- - `issuer` (`string`) — The authorization server's issuer identifier (its base URL); used to build absolute endpoint URLs in the discovery document
443
- - `clientStore` (`ClientStore`) — `{ get(clientId), register(client) }` for dynamic clients
444
- - `authCodeStore` (`AuthCodeStore`) — `{ save(code), get(code), delete(code) }` for short-lived authorization codes (codes are single-use)
445
- - `issueTokens` (`({ subject, scopes, client }) => IssuedTokens`) — App-owned token minting; returns `{ accessToken, refreshToken?, expiresIn?, scope? }`
446
- - `onAuthorize` (`({ ctx, client, request }) => OnAuthorizeResult`) — App-owned login/consent; return `{ approved: true, subject, scopes? }` to issue a code, or take over the response and return `{ approved: false }`
447
- - `onRefreshToken` (`({ refreshToken, client, scopes }) => { subject, scopes } | undefined`, optional) — App-owned refresh-token validation; required to enable the `refresh_token` grant
448
- - `scopesSupported` (`string[]`, optional) — Advertised in discovery metadata as `scopes_supported`
449
- - `resource` (`string`, optional) — When set, also serves `/.well-known/oauth-protected-resource`
450
- - `authorizationCodeTtl` (`number`, optional) — Authorization code lifetime in seconds (default: `600`)
451
- - `endpoints` (`{ authorize?, token?, register? }`, optional) — Override the default endpoint paths
452
-
453
- **Returns:** `Router` — Koa router instance exposing `routes()` / `allowedMethods()`
454
-
455
371
  ### `apiCall(method, url, options?)`
456
372
 
457
373
  Generic HTTP helper for use inside MCP tool handlers.
package/dist/index.cjs CHANGED
@@ -4,453 +4,29 @@ Object.defineProperty(exports, Symbol.toStringTag, {
4
4
  });
5
5
  let node_async_hooks = require("node:async_hooks");
6
6
  let _modelcontextprotocol_sdk_server_streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
7
+ let _ttoss_auth_core_amazon_cognito = require("@ttoss/auth-core/amazon-cognito");
7
8
  let _ttoss_http_server = require("@ttoss/http-server");
9
+ let _ttoss_http_server_auth = require("@ttoss/http-server-auth");
8
10
  let zod = require("zod");
9
- let _ttoss_auth_core_amazon_cognito = require("@ttoss/auth-core/amazon-cognito");
10
- let node_crypto = require("node:crypto");
11
11
  let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
12
12
 
13
- //#region src/auth.ts
13
+ //#region src/index.ts
14
14
  /** MCP lifecycle/discovery methods reachable before a client authenticates. */
15
15
  var DEFAULT_PUBLIC_METHODS = ["initialize", "tools/list"];
16
- /**
17
- * Builds the token verifier from the auth options, preferring an explicit
18
- * `verifyToken` and otherwise creating a `CognitoJwtVerifier`.
19
- */
20
- var buildTokenVerifier = auth => {
16
+ /** Builds the token verifier from the MCP auth options. */
17
+ var buildVerifyToken = auth => {
21
18
  if (auth.cognitoUserPool) {
22
- const v = _ttoss_auth_core_amazon_cognito.CognitoJwtVerifier.create({
19
+ const verifier = _ttoss_auth_core_amazon_cognito.CognitoJwtVerifier.create({
23
20
  tokenUse: "access",
24
21
  ...auth.cognitoUserPool
25
22
  });
26
- return t => {
27
- return v.verify(t);
23
+ return token => {
24
+ return verifier.verify(token);
28
25
  };
29
26
  }
30
27
  if (auth.verifyToken) return auth.verifyToken;
31
28
  throw new Error("McpAuthOptions requires either cognitoUserPool or verifyToken");
32
29
  };
33
- /** Returns true when the identity carries every required scope (or none are required). */
34
- var hasRequiredScopes = (requiredScopes, identity) => {
35
- if (!requiredScopes?.length) return true;
36
- const tokenScopes = (identity?.scope ?? "").split(" ");
37
- return requiredScopes.every(s => {
38
- return tokenScopes.includes(s);
39
- });
40
- };
41
- /**
42
- * Registers the token-verification middleware (with public-method bypass and
43
- * RFC 9728 discovery) and, when configured, the OAuth protected-resource
44
- * metadata endpoint on the given router.
45
- */
46
- var registerAuthRoutes = (router, path, auth, tokenVerifier) => {
47
- const publicMethods = new Set(auth.publicMethods ?? DEFAULT_PUBLIC_METHODS);
48
- const unauthorizedHeader = auth.resourceMetadataUrl ? `Bearer resource_metadata="${auth.resourceMetadataUrl}"` : "Bearer";
49
- router.use(path, async (ctx, next) => {
50
- const method = ctx.request.body?.method;
51
- if (publicMethods.has(method ?? "")) {
52
- await next();
53
- return;
54
- }
55
- const authHeader = ctx.headers.authorization;
56
- const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : "";
57
- let identity;
58
- try {
59
- identity = await tokenVerifier(token);
60
- } catch {
61
- ctx.status = 401;
62
- ctx.set("WWW-Authenticate", unauthorizedHeader);
63
- ctx.body = "Unauthorized";
64
- return;
65
- }
66
- if (!hasRequiredScopes(auth.requiredScopes, identity)) {
67
- ctx.status = 403;
68
- ctx.body = "Forbidden";
69
- return;
70
- }
71
- ctx.state.identity = identity;
72
- await next();
73
- });
74
- if (auth.resourceServerUrl && auth.authorizationServerUrl) router.get("/.well-known/oauth-protected-resource", ctx => {
75
- ctx.body = {
76
- resource: auth.resourceServerUrl,
77
- authorization_servers: [auth.authorizationServerUrl]
78
- };
79
- });
80
- };
81
-
82
- //#endregion
83
- //#region src/authServerHandlers.ts
84
- var base64UrlEncode = buffer => {
85
- return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
86
- };
87
- /** Generates a URL-safe random token of the given byte length. */
88
- var generateToken = (bytes = 32) => {
89
- return base64UrlEncode((0, node_crypto.randomBytes)(bytes));
90
- };
91
- var sha256Base64Url = input => {
92
- return base64UrlEncode((0, node_crypto.createHash)("sha256").update(input).digest());
93
- };
94
- /** Verifies a PKCE `code_verifier` against a stored S256 `code_challenge`. */
95
- var verifyPkce = (codeVerifier, codeChallenge) => {
96
- return sha256Base64Url(codeVerifier) === codeChallenge;
97
- };
98
- /** Joins an issuer base URL with a path, collapsing any duplicate slash. */
99
- var joinUrl = (issuer, path) => {
100
- return `${issuer.replace(/\/$/, "")}${path}`;
101
- };
102
- /** Returns the value if it is a string, otherwise `undefined`. */
103
- var asString = value => {
104
- return typeof value === "string" ? value : void 0;
105
- };
106
- var parseScopes = scope => {
107
- const value = asString(scope);
108
- return value ? value.split(" ").filter(Boolean) : [];
109
- };
110
- /** Writes a spec-compliant OAuth error response (RFC 6749 §5.2). */
111
- var sendOAuthError = (ctx, status, error, description) => {
112
- ctx.status = status;
113
- ctx.body = {
114
- error,
115
- error_description: description
116
- };
117
- };
118
- var buildTokenResponse = (tokens, scopes) => {
119
- return {
120
- access_token: tokens.accessToken,
121
- token_type: "Bearer",
122
- ...(tokens.expiresIn !== void 0 ? {
123
- expires_in: tokens.expiresIn
124
- } : {}),
125
- ...(tokens.refreshToken !== void 0 ? {
126
- refresh_token: tokens.refreshToken
127
- } : {}),
128
- scope: tokens.scope ?? scopes.join(" ")
129
- };
130
- };
131
- /**
132
- * Authenticates the client at the token endpoint via `client_secret_basic`
133
- * (Authorization header), `client_secret_post` (body), or `none` (public,
134
- * PKCE-only). Returns the client, or `undefined` when authentication fails.
135
- */
136
- var authenticateClient = async (ctx, body, clientStore) => {
137
- let clientId = asString(body.client_id);
138
- let clientSecret = asString(body.client_secret);
139
- const authHeader = ctx.headers.authorization;
140
- if (authHeader?.startsWith("Basic ")) {
141
- const decoded = Buffer.from(authHeader.slice(6), "base64").toString();
142
- const separatorIndex = decoded.indexOf(":");
143
- if (separatorIndex !== -1) {
144
- clientId = decodeURIComponent(decoded.slice(0, separatorIndex));
145
- clientSecret = decodeURIComponent(decoded.slice(separatorIndex + 1));
146
- }
147
- }
148
- if (!clientId) return;
149
- const client = await clientStore.get(clientId);
150
- if (!client) return;
151
- if (client.client_secret && client.client_secret !== clientSecret) return;
152
- return client;
153
- };
154
- /** Handles `grant_type=authorization_code` token requests (with PKCE verify). */
155
- var handleAuthorizationCodeGrant = async (ctx, body, options) => {
156
- const {
157
- clientStore,
158
- authCodeStore,
159
- issueTokens
160
- } = options;
161
- const client = await authenticateClient(ctx, body, clientStore);
162
- if (!client) {
163
- sendOAuthError(ctx, 401, "invalid_client", "client authentication failed");
164
- return;
165
- }
166
- const code = asString(body.code);
167
- if (!code) {
168
- sendOAuthError(ctx, 400, "invalid_request", "code is required");
169
- return;
170
- }
171
- const stored = await authCodeStore.get(code);
172
- await authCodeStore.delete(code);
173
- if (!stored) {
174
- sendOAuthError(ctx, 400, "invalid_grant", "invalid or expired authorization code");
175
- return;
176
- }
177
- if (stored.expiresAt < Date.now()) {
178
- sendOAuthError(ctx, 400, "invalid_grant", "authorization code expired");
179
- return;
180
- }
181
- if (stored.clientId !== client.client_id) {
182
- sendOAuthError(ctx, 400, "invalid_grant", "authorization code was not issued to this client");
183
- return;
184
- }
185
- if (stored.redirectUri !== asString(body.redirect_uri)) {
186
- sendOAuthError(ctx, 400, "invalid_grant", "redirect_uri mismatch");
187
- return;
188
- }
189
- const codeVerifier = asString(body.code_verifier);
190
- if (!codeVerifier) {
191
- sendOAuthError(ctx, 400, "invalid_request", "code_verifier is required");
192
- return;
193
- }
194
- if (!verifyPkce(codeVerifier, stored.codeChallenge)) {
195
- sendOAuthError(ctx, 400, "invalid_grant", "PKCE verification failed");
196
- return;
197
- }
198
- ctx.body = buildTokenResponse(await issueTokens({
199
- subject: stored.subject,
200
- scopes: stored.scopes,
201
- client
202
- }), stored.scopes);
203
- };
204
- /** Handles `grant_type=refresh_token` token requests. */
205
- var handleRefreshTokenGrant = async (ctx, body, options) => {
206
- const {
207
- clientStore,
208
- issueTokens,
209
- onRefreshToken
210
- } = options;
211
- const client = await authenticateClient(ctx, body, clientStore);
212
- if (!client) {
213
- sendOAuthError(ctx, 401, "invalid_client", "client authentication failed");
214
- return;
215
- }
216
- if (!onRefreshToken) {
217
- sendOAuthError(ctx, 400, "unsupported_grant_type", "refresh_token grant is not supported");
218
- return;
219
- }
220
- const refreshToken = asString(body.refresh_token);
221
- if (!refreshToken) {
222
- sendOAuthError(ctx, 400, "invalid_request", "refresh_token is required");
223
- return;
224
- }
225
- const result = await onRefreshToken({
226
- refreshToken,
227
- client,
228
- scopes: parseScopes(body.scope)
229
- });
230
- if (!result) {
231
- sendOAuthError(ctx, 400, "invalid_grant", "invalid refresh token");
232
- return;
233
- }
234
- ctx.body = buildTokenResponse(await issueTokens({
235
- subject: result.subject,
236
- scopes: result.scopes,
237
- client
238
- }), result.scopes);
239
- };
240
- /**
241
- * Handles the authorization request after `client_id` and `redirect_uri` have
242
- * been validated: enforces PKCE, runs the consent hook, and issues a code.
243
- */
244
- var handleAuthorize = async (ctx, options, redirectUri) => {
245
- const {
246
- clientStore,
247
- authCodeStore,
248
- onAuthorize
249
- } = options;
250
- const ttl = options.authorizationCodeTtl ?? 600;
251
- const clientId = asString(ctx.query.client_id);
252
- const client = await clientStore.get(clientId);
253
- const state = asString(ctx.query.state);
254
- const redirectError = (error, description) => {
255
- const url = new URL(redirectUri);
256
- url.searchParams.set("error", error);
257
- url.searchParams.set("error_description", description);
258
- if (state) url.searchParams.set("state", state);
259
- ctx.redirect(url.toString());
260
- };
261
- if (asString(ctx.query.response_type) !== "code") {
262
- redirectError("unsupported_response_type", "response_type must be code");
263
- return;
264
- }
265
- const codeChallenge = asString(ctx.query.code_challenge);
266
- if (!codeChallenge) {
267
- redirectError("invalid_request", "code_challenge is required (PKCE)");
268
- return;
269
- }
270
- const codeChallengeMethod = asString(ctx.query.code_challenge_method) ?? "plain";
271
- if (codeChallengeMethod !== "S256") {
272
- redirectError("invalid_request", "code_challenge_method must be S256");
273
- return;
274
- }
275
- const scopes = parseScopes(ctx.query.scope);
276
- const result = await onAuthorize({
277
- ctx,
278
- client,
279
- request: {
280
- clientId,
281
- redirectUri,
282
- scopes,
283
- state,
284
- codeChallenge,
285
- codeChallengeMethod
286
- }
287
- });
288
- if (!result.approved) return;
289
- const grantedScopes = result.scopes ?? scopes;
290
- const code = generateToken();
291
- await authCodeStore.save({
292
- code,
293
- clientId,
294
- redirectUri,
295
- codeChallenge,
296
- scopes: grantedScopes,
297
- subject: result.subject,
298
- expiresAt: Date.now() + ttl * 1e3
299
- });
300
- const url = new URL(redirectUri);
301
- url.searchParams.set("code", code);
302
- if (state) url.searchParams.set("state", state);
303
- ctx.redirect(url.toString());
304
- };
305
- /** Handles Dynamic Client Registration (RFC 7591) requests. */
306
- var handleRegister = async (ctx, clientStore) => {
307
- const metadata = ctx.request.body ?? {};
308
- const redirectUris = metadata.redirect_uris;
309
- if (!Array.isArray(redirectUris) || redirectUris.length === 0 || !redirectUris.every(uri => {
310
- return typeof uri === "string";
311
- })) {
312
- sendOAuthError(ctx, 400, "invalid_redirect_uri", "redirect_uris is required and must be an array of strings");
313
- return;
314
- }
315
- const tokenAuthMethod = asString(metadata.token_endpoint_auth_method) ?? "client_secret_basic";
316
- const isPublic = tokenAuthMethod === "none";
317
- const client = {
318
- ...metadata,
319
- client_id: generateToken(16),
320
- redirect_uris: redirectUris,
321
- token_endpoint_auth_method: tokenAuthMethod,
322
- grant_types: metadata.grant_types ?? ["authorization_code", "refresh_token"],
323
- response_types: metadata.response_types ?? ["code"],
324
- client_id_issued_at: Math.floor(Date.now() / 1e3)
325
- };
326
- if (!isPublic) {
327
- client.client_secret = generateToken(32);
328
- client.client_secret_expires_at = 0;
329
- }
330
- await clientStore.register(client);
331
- ctx.status = 201;
332
- ctx.body = client;
333
- };
334
-
335
- //#endregion
336
- //#region src/authServer.ts
337
- /**
338
- * Creates transport-agnostic OAuth 2.1 Authorization Server primitives for MCP.
339
- *
340
- * Mounts the authorization endpoint (`/authorize`, PKCE S256 required), token
341
- * endpoint (`/token`, `authorization_code` + `refresh_token` grants), Dynamic
342
- * Client Registration (`/register`, RFC 7591), and AS metadata discovery
343
- * (`/.well-known/oauth-authorization-server`, RFC 8414). When `resource` is
344
- * set, it also serves the protected-resource metadata (RFC 9728).
345
- *
346
- * ttoss owns only the protocol mechanics — the app supplies its own stores,
347
- * token signing/verification, and login/consent UI through the option hooks, so
348
- * the user model, JWT/IAM, and authentication never leave the consuming app.
349
- *
350
- * @param options - Authorization server configuration and pluggable hooks.
351
- * @returns A Koa `Router` exposing `routes()` / `allowedMethods()`.
352
- *
353
- * @example
354
- * ```typescript
355
- * import { App, bodyParser } from '@ttoss/http-server';
356
- * import { createMcpAuthServer } from '@ttoss/http-server-mcp';
357
- *
358
- * const authServer = createMcpAuthServer({
359
- * issuer: 'https://api.soat.dev',
360
- * clientStore,
361
- * authCodeStore,
362
- * issueTokens: async ({ subject, scopes }) => ({
363
- * accessToken: signJwt({ sub: subject, scope: scopes.join(' ') }),
364
- * refreshToken: createRefreshToken(subject),
365
- * expiresIn: 3600,
366
- * }),
367
- * onAuthorize: async ({ ctx }) => {
368
- * const session = await getSession(ctx);
369
- * if (!session) {
370
- * ctx.redirect('/login');
371
- * return { approved: false };
372
- * }
373
- * return { approved: true, subject: session.userId };
374
- * },
375
- * scopesSupported: ['mcp:access'],
376
- * });
377
- *
378
- * const app = new App();
379
- * app.use(bodyParser());
380
- * app.use(authServer.routes());
381
- * ```
382
- */
383
- var createMcpAuthServer = options => {
384
- const {
385
- issuer,
386
- clientStore,
387
- scopesSupported,
388
- resource
389
- } = options;
390
- const authorizePath = options.endpoints?.authorize ?? "/authorize";
391
- const tokenPath = options.endpoints?.token ?? "/token";
392
- const registerPath = options.endpoints?.register ?? "/register";
393
- const router = new _ttoss_http_server.Router();
394
- router.get("/.well-known/oauth-authorization-server", ctx => {
395
- ctx.body = {
396
- issuer,
397
- authorization_endpoint: joinUrl(issuer, authorizePath),
398
- token_endpoint: joinUrl(issuer, tokenPath),
399
- registration_endpoint: joinUrl(issuer, registerPath),
400
- response_types_supported: ["code"],
401
- grant_types_supported: ["authorization_code", "refresh_token"],
402
- code_challenge_methods_supported: ["S256"],
403
- token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post", "none"],
404
- ...(scopesSupported ? {
405
- scopes_supported: scopesSupported
406
- } : {})
407
- };
408
- });
409
- if (resource) router.get("/.well-known/oauth-protected-resource", ctx => {
410
- ctx.body = {
411
- resource,
412
- authorization_servers: [issuer]
413
- };
414
- });
415
- router.get(authorizePath, async ctx => {
416
- const clientId = asString(ctx.query.client_id);
417
- if (!clientId) {
418
- sendOAuthError(ctx, 400, "invalid_request", "client_id is required");
419
- return;
420
- }
421
- const client = await clientStore.get(clientId);
422
- if (!client) {
423
- sendOAuthError(ctx, 400, "invalid_client", "unknown client_id");
424
- return;
425
- }
426
- const redirectUri = asString(ctx.query.redirect_uri);
427
- if (!redirectUri || !client.redirect_uris.includes(redirectUri)) {
428
- sendOAuthError(ctx, 400, "invalid_request", "redirect_uri is missing or not registered for this client");
429
- return;
430
- }
431
- await handleAuthorize(ctx, options, redirectUri);
432
- });
433
- router.post(tokenPath, async ctx => {
434
- const body = ctx.request.body ?? {};
435
- const grantType = asString(body.grant_type);
436
- if (grantType === "authorization_code") {
437
- await handleAuthorizationCodeGrant(ctx, body, options);
438
- return;
439
- }
440
- if (grantType === "refresh_token") {
441
- await handleRefreshTokenGrant(ctx, body, options);
442
- return;
443
- }
444
- sendOAuthError(ctx, 400, "unsupported_grant_type", "grant_type must be authorization_code or refresh_token");
445
- });
446
- router.post(registerPath, async ctx => {
447
- await handleRegister(ctx, clientStore);
448
- });
449
- return router;
450
- };
451
-
452
- //#endregion
453
- //#region src/index.ts
454
30
  var requestContextStore = new node_async_hooks.AsyncLocalStorage();
455
31
  /**
456
32
  * Generic HTTP helper for use inside MCP tool handlers.
@@ -630,10 +206,37 @@ var createMcpRouter = (server, options = {}) => {
630
206
  return result;
631
207
  };
632
208
  const router = new _ttoss_http_server.Router();
633
- if (auth) registerAuthRoutes(router, path, auth, buildTokenVerifier(auth));
209
+ if (auth) {
210
+ const verifyToken = buildVerifyToken(auth);
211
+ const publicMethods = new Set(auth.publicMethods ?? DEFAULT_PUBLIC_METHODS);
212
+ const verify = (0, _ttoss_http_server_auth.authMiddleware)({
213
+ strategies: ["oauth"],
214
+ oauth: {
215
+ verify: token => {
216
+ return verifyToken(token);
217
+ },
218
+ requiredScopes: auth.requiredScopes
219
+ },
220
+ resourceMetadataUrl: auth.resourceMetadataUrl
221
+ });
222
+ router.use(path, async (ctx, next) => {
223
+ const method = ctx.request.body?.method;
224
+ if (publicMethods.has(method ?? "")) {
225
+ await next();
226
+ return;
227
+ }
228
+ await verify(ctx, next);
229
+ });
230
+ if (auth.resourceServerUrl && auth.authorizationServerUrl) router.get("/.well-known/oauth-protected-resource", ctx => {
231
+ ctx.body = {
232
+ resource: auth.resourceServerUrl,
233
+ authorization_servers: [auth.authorizationServerUrl]
234
+ };
235
+ });
236
+ }
634
237
  const handleWithContext = async (ctx, body) => {
635
238
  const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
636
- const identity = ctx.state.identity;
239
+ const identity = ctx.state.user;
637
240
  const runRequest = async transport => {
638
241
  await transport.handleRequest(ctx.req, ctx.res, body);
639
242
  ctx.respond = false;
@@ -775,62 +378,6 @@ var registerToolFromSchema = (server, params) => {
775
378
  });
776
379
  }
777
380
  };
778
- /**
779
- * Returns the `WWW-Authenticate` header value for a 401 response on a
780
- * protected resource, following the MCP auth spec requirement that
781
- * unauthorized responses advertise the resource metadata URL so MCP
782
- * clients can bootstrap OAuth discovery.
783
- *
784
- * Use this in your own auth middleware when you are not using the built-in
785
- * `auth` option on `createMcpRouter`.
786
- *
787
- * @example
788
- * ```typescript
789
- * import { getWwwAuthenticateHeader } from '@ttoss/http-server-mcp';
790
- *
791
- * // Inside a Koa middleware
792
- * ctx.status = 401;
793
- * ctx.set('WWW-Authenticate', getWwwAuthenticateHeader({ resource: 'https://mcp.example.com' }));
794
- * ```
795
- */
796
- var getWwwAuthenticateHeader = args => {
797
- return `Bearer resource_metadata="${`${args.resource.replace(/\/$/, "")}/.well-known/oauth-protected-resource`}"`;
798
- };
799
- /**
800
- * Creates a standalone Koa middleware that serves
801
- * `GET /.well-known/oauth-protected-resource` (RFC 9728) without requiring
802
- * the built-in `auth` option on `createMcpRouter`.
803
- *
804
- * Mount this **before** your own auth middleware so the discovery endpoint
805
- * remains unauthenticated (MCP clients fetch it before they have a token).
806
- *
807
- * @example
808
- * ```typescript
809
- * import Koa from 'koa';
810
- * import { createProtectedResourceMetadataMiddleware } from '@ttoss/http-server-mcp';
811
- *
812
- * const app = new Koa();
813
- * app.use(
814
- * createProtectedResourceMetadataMiddleware({
815
- * resource: 'https://mcp.example.com',
816
- * authorizationServers: ['https://api.example.com'],
817
- * })
818
- * );
819
- * app.use(myOwnAuthMiddleware);
820
- * ```
821
- */
822
- var createProtectedResourceMetadataMiddleware = args => {
823
- return async (ctx, next) => {
824
- if (ctx.method === "GET" && ctx.path === "/.well-known/oauth-protected-resource") {
825
- ctx.body = {
826
- resource: args.resource,
827
- authorization_servers: args.authorizationServers
828
- };
829
- return;
830
- }
831
- await next();
832
- };
833
- };
834
381
 
835
382
  //#endregion
836
383
  Object.defineProperty(exports, 'McpServer', {
@@ -841,11 +388,8 @@ Object.defineProperty(exports, 'McpServer', {
841
388
  });
842
389
  exports.apiCall = apiCall;
843
390
  exports.checkScopes = checkScopes;
844
- exports.createMcpAuthServer = createMcpAuthServer;
845
391
  exports.createMcpRouter = createMcpRouter;
846
- exports.createProtectedResourceMetadataMiddleware = createProtectedResourceMetadataMiddleware;
847
392
  exports.getIdentity = getIdentity;
848
- exports.getWwwAuthenticateHeader = getWwwAuthenticateHeader;
849
393
  exports.registerToolFromSchema = registerToolFromSchema;
850
394
  Object.defineProperty(exports, 'z', {
851
395
  enumerable: true,