@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 +24 -1
- package/dist/index.cjs +26 -12
- package/dist/index.d.cts +11 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.mjs +26 -12
- package/package.json +1 -1
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
|
-
|
|
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)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|