@ttoss/http-server-mcp 0.17.1 → 0.18.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/README.md CHANGED
@@ -279,6 +279,8 @@ createMcpRouter(mcpServer, {
279
279
  });
280
280
  ```
281
281
 
282
+ The `resource` field in the metadata document is automatically set to `resourceServerUrl + path` (e.g. `https://mcp.example.com/mcp` for the default path). This means MCP clients that follow `resource` to connect will land on the actual MCP endpoint rather than the bare origin.
283
+
282
284
  **With your own auth middleware** — use `createProtectedResourceMetadataMiddleware` as a standalone middleware, mounted _before_ your auth layer so discovery stays unauthenticated:
283
285
 
284
286
  ```typescript
@@ -340,6 +342,26 @@ createMcpRouter(mcpServer, {
340
342
 
341
343
  Both fields are optional. Omitting `resourceMetadataUrl` keeps the bare `Bearer` header, and the `publicMethods` default matches what MCP clients expect for discovery.
342
344
 
345
+ ### Supporting clients that connect to the bare origin
346
+
347
+ Some MCP clients always POST to the bare origin (`/`) regardless of the `resource` value in the metadata. To serve both behaviors from the same router, use the `aliases` option:
348
+
349
+ ```typescript
350
+ createMcpRouter(mcpServer, {
351
+ // The primary endpoint — also the value advertised as `resource` in metadata.
352
+ path: '/mcp',
353
+ // Additionally handle requests at the bare root for clients that ignore `resource`.
354
+ aliases: ['/'],
355
+ auth: {
356
+ verifyToken: async (token) => myVerify(token),
357
+ resourceServerUrl: 'https://mcp.example.com',
358
+ authorizationServerUrl: 'https://auth.example.com',
359
+ },
360
+ });
361
+ ```
362
+
363
+ The discovery endpoint (`/.well-known/oauth-protected-resource`) remains publicly accessible even when `aliases` includes `'/'`.
364
+
343
365
  ## Issuing tokens for MCP clients
344
366
 
345
367
  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.
@@ -355,6 +377,7 @@ Creates a Koa router configured to handle MCP protocol requests.
355
377
  - `server` (`McpServer`) — MCP server instance with registered tools and resources
356
378
  - `options` (`McpRouterOptions`) — Optional configuration
357
379
  - `path` (`string`) — HTTP path for MCP endpoint (default: `'/mcp'`)
380
+ - `aliases` (`string[]`) — Additional paths where the MCP handler is also mounted; use `['/']` to also handle requests at the bare root (default: `[]`)
358
381
  - `sessionIdGenerator` (`() => string`) — Session ID generator for stateful servers (default: `undefined` for stateless)
359
382
  - `apiBaseUrl` (`string`) — Base URL prepended to relative paths in `apiCall`
360
383
  - `getApiHeaders` (`(ctx: Context) => Record<string, string>`) — Return headers to inject into every `apiCall` for this request
@@ -362,7 +385,7 @@ Creates a Koa router configured to handle MCP protocol requests.
362
385
  - `auth.cognitoUserPool` — Cognito user pool config (`userPoolId`, `clientId`, `tokenUse`)
363
386
  - `auth.verifyToken` — Custom async token verifier `(token: string) => Promise<unknown>`
364
387
  - `auth.requiredScopes` — Router-level scope guard; returns 403 if any scope is missing
365
- - `auth.resourceServerUrl` + `auth.authorizationServerUrl` — Enable `/.well-known/oauth-protected-resource`
388
+ - `auth.resourceServerUrl` + `auth.authorizationServerUrl` — Enable `/.well-known/oauth-protected-resource`; the metadata `resource` is set to `resourceServerUrl + path` so clients following `resource` land on the actual MCP endpoint
366
389
  - `auth.publicMethods` — JSON-RPC methods that bypass verification (default `['initialize', 'tools/list']`)
367
390
  - `auth.resourceMetadataUrl` — Emit RFC 9728 `WWW-Authenticate: Bearer resource_metadata="…"` on 401
368
391
 
package/dist/index.cjs CHANGED
@@ -190,6 +190,7 @@ var checkScopes = required => {
190
190
  var createMcpRouter = (server, options = {}) => {
191
191
  const {
192
192
  path = "/mcp",
193
+ aliases = [],
193
194
  sessionIdGenerator,
194
195
  apiBaseUrl,
195
196
  getApiHeaders,
@@ -214,6 +215,7 @@ var createMcpRouter = (server, options = {}) => {
214
215
  return result;
215
216
  };
216
217
  const router = new _ttoss_http_server.Router();
218
+ let authCheck;
217
219
  if (auth) {
218
220
  const verifyToken = buildVerifyToken(auth);
219
221
  const publicMethods = new Set(auth.publicMethods ?? DEFAULT_PUBLIC_METHODS);
@@ -227,20 +229,24 @@ var createMcpRouter = (server, options = {}) => {
227
229
  },
228
230
  resourceMetadataUrl: auth.resourceMetadataUrl
229
231
  });
230
- router.use(path, async (ctx, next) => {
232
+ authCheck = async (ctx, next) => {
231
233
  const method = ctx.request.body?.method;
232
234
  if (publicMethods.has(method ?? "")) {
233
235
  await next();
234
236
  return;
235
237
  }
236
238
  await verify(ctx, next);
237
- });
238
- if (auth.resourceServerUrl && auth.authorizationServerUrl) router.get("/.well-known/oauth-protected-resource", ctx => {
239
- ctx.body = {
240
- resource: auth.resourceServerUrl,
241
- authorization_servers: [auth.authorizationServerUrl]
242
- };
243
- });
239
+ };
240
+ if (auth.resourceServerUrl && auth.authorizationServerUrl) {
241
+ const base = auth.resourceServerUrl.replace(/\/$/, "");
242
+ const resourceUrl = path === "/" ? base : `${base}${path}`;
243
+ router.get("/.well-known/oauth-protected-resource", ctx => {
244
+ ctx.body = {
245
+ resource: resourceUrl,
246
+ authorization_servers: [auth.authorizationServerUrl]
247
+ };
248
+ });
249
+ }
244
250
  }
245
251
  const handleWithContext = async (ctx, body) => {
246
252
  const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
@@ -276,7 +282,7 @@ var createMcpRouter = (server, options = {}) => {
276
282
  }
277
283
  });
278
284
  };
279
- router.post(path, async ctx => {
285
+ const postHandler = async ctx => {
280
286
  try {
281
287
  await handleWithContext(ctx, ctx.request.body);
282
288
  } catch (error) {
@@ -287,8 +293,8 @@ var createMcpRouter = (server, options = {}) => {
287
293
  };
288
294
  }
289
295
  }
290
- });
291
- router.delete(path, async ctx => {
296
+ };
297
+ const deleteHandler = async ctx => {
292
298
  try {
293
299
  await handleWithContext(ctx);
294
300
  } catch (error) {
@@ -299,7 +305,15 @@ var createMcpRouter = (server, options = {}) => {
299
305
  };
300
306
  }
301
307
  }
302
- });
308
+ };
309
+ const allPaths = [path, ...aliases];
310
+ for (const mcpPath of allPaths) if (authCheck) {
311
+ router.post(mcpPath, authCheck, postHandler);
312
+ router.delete(mcpPath, authCheck, deleteHandler);
313
+ } else {
314
+ router.post(mcpPath, postHandler);
315
+ router.delete(mcpPath, deleteHandler);
316
+ }
303
317
  return router;
304
318
  };
305
319
  var rawSchemaRegistryMap = /* @__PURE__ */new WeakMap();
package/dist/index.d.cts CHANGED
@@ -819,6 +819,17 @@ interface McpRouterOptions {
819
819
  * @default '/mcp'
820
820
  */
821
821
  path?: string;
822
+ /**
823
+ * Additional HTTP paths where the MCP server is also mounted.
824
+ *
825
+ * Useful when MCP clients differ in where they connect after OAuth: some
826
+ * follow the protected-resource `resource` metadata value as the endpoint,
827
+ * others always connect to the bare origin (`/`). Setting `aliases: ['/']`
828
+ * serves both without requiring app-level path rewrites.
829
+ *
830
+ * @example ['/'] // also handle MCP requests at the bare root
831
+ */
832
+ aliases?: string[];
822
833
  /**
823
834
  * Optional session ID generator for stateful MCP servers.
824
835
  * When provided, a single shared transport is created and sessions are tracked.
package/dist/index.d.mts CHANGED
@@ -819,6 +819,17 @@ interface McpRouterOptions {
819
819
  * @default '/mcp'
820
820
  */
821
821
  path?: string;
822
+ /**
823
+ * Additional HTTP paths where the MCP server is also mounted.
824
+ *
825
+ * Useful when MCP clients differ in where they connect after OAuth: some
826
+ * follow the protected-resource `resource` metadata value as the endpoint,
827
+ * others always connect to the bare origin (`/`). Setting `aliases: ['/']`
828
+ * serves both without requiring app-level path rewrites.
829
+ *
830
+ * @example ['/'] // also handle MCP requests at the bare root
831
+ */
832
+ aliases?: string[];
822
833
  /**
823
834
  * Optional session ID generator for stateful MCP servers.
824
835
  * When provided, a single shared transport is created and sessions are tracked.
package/dist/index.mjs CHANGED
@@ -187,6 +187,7 @@ var checkScopes = required => {
187
187
  var createMcpRouter = (server, options = {}) => {
188
188
  const {
189
189
  path = "/mcp",
190
+ aliases = [],
190
191
  sessionIdGenerator,
191
192
  apiBaseUrl,
192
193
  getApiHeaders,
@@ -211,6 +212,7 @@ var createMcpRouter = (server, options = {}) => {
211
212
  return result;
212
213
  };
213
214
  const router = new Router();
215
+ let authCheck;
214
216
  if (auth) {
215
217
  const verifyToken = buildVerifyToken(auth);
216
218
  const publicMethods = new Set(auth.publicMethods ?? DEFAULT_PUBLIC_METHODS);
@@ -224,20 +226,24 @@ var createMcpRouter = (server, options = {}) => {
224
226
  },
225
227
  resourceMetadataUrl: auth.resourceMetadataUrl
226
228
  });
227
- router.use(path, async (ctx, next) => {
229
+ authCheck = async (ctx, next) => {
228
230
  const method = ctx.request.body?.method;
229
231
  if (publicMethods.has(method ?? "")) {
230
232
  await next();
231
233
  return;
232
234
  }
233
235
  await verify(ctx, next);
234
- });
235
- if (auth.resourceServerUrl && auth.authorizationServerUrl) router.get("/.well-known/oauth-protected-resource", ctx => {
236
- ctx.body = {
237
- resource: auth.resourceServerUrl,
238
- authorization_servers: [auth.authorizationServerUrl]
239
- };
240
- });
236
+ };
237
+ if (auth.resourceServerUrl && auth.authorizationServerUrl) {
238
+ const base = auth.resourceServerUrl.replace(/\/$/, "");
239
+ const resourceUrl = path === "/" ? base : `${base}${path}`;
240
+ router.get("/.well-known/oauth-protected-resource", ctx => {
241
+ ctx.body = {
242
+ resource: resourceUrl,
243
+ authorization_servers: [auth.authorizationServerUrl]
244
+ };
245
+ });
246
+ }
241
247
  }
242
248
  const handleWithContext = async (ctx, body) => {
243
249
  const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
@@ -273,7 +279,7 @@ var createMcpRouter = (server, options = {}) => {
273
279
  }
274
280
  });
275
281
  };
276
- router.post(path, async ctx => {
282
+ const postHandler = async ctx => {
277
283
  try {
278
284
  await handleWithContext(ctx, ctx.request.body);
279
285
  } catch (error) {
@@ -284,8 +290,8 @@ var createMcpRouter = (server, options = {}) => {
284
290
  };
285
291
  }
286
292
  }
287
- });
288
- router.delete(path, async ctx => {
293
+ };
294
+ const deleteHandler = async ctx => {
289
295
  try {
290
296
  await handleWithContext(ctx);
291
297
  } catch (error) {
@@ -296,7 +302,15 @@ var createMcpRouter = (server, options = {}) => {
296
302
  };
297
303
  }
298
304
  }
299
- });
305
+ };
306
+ const allPaths = [path, ...aliases];
307
+ for (const mcpPath of allPaths) if (authCheck) {
308
+ router.post(mcpPath, authCheck, postHandler);
309
+ router.delete(mcpPath, authCheck, deleteHandler);
310
+ } else {
311
+ router.post(mcpPath, postHandler);
312
+ router.delete(mcpPath, deleteHandler);
313
+ }
300
314
  return router;
301
315
  };
302
316
  var rawSchemaRegistryMap = /* @__PURE__ */new WeakMap();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttoss/http-server-mcp",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
5
5
  "keywords": [
6
6
  "ai",