@ttoss/http-server-mcp 0.17.0 → 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 +36 -14
- package/dist/index.d.cts +19 -2
- package/dist/index.d.mts +19 -2
- package/dist/index.mjs +36 -14
- package/package.json +3 -3
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
|
@@ -126,7 +126,9 @@ var getIdentity = () => {
|
|
|
126
126
|
* Throws if any scope is missing — the MCP SDK catches this and returns a
|
|
127
127
|
* tool error to the client. Use inside tool handlers for per-tool authorization.
|
|
128
128
|
*
|
|
129
|
-
*
|
|
129
|
+
* Accepts either `scope: string` (space-separated, standard JWT claim) or
|
|
130
|
+
* `scopes: string[]` from `verifyToken`. If neither is present and `required`
|
|
131
|
+
* is non-empty, throws with a descriptive message instead of a silent 403.
|
|
130
132
|
*
|
|
131
133
|
* @example
|
|
132
134
|
* ```typescript
|
|
@@ -137,7 +139,13 @@ var getIdentity = () => {
|
|
|
137
139
|
* ```
|
|
138
140
|
*/
|
|
139
141
|
var checkScopes = required => {
|
|
140
|
-
const
|
|
142
|
+
const identity = getIdentity();
|
|
143
|
+
let scopeString = null;
|
|
144
|
+
if (typeof identity?.scope === "string") scopeString = identity.scope;else if (Array.isArray(identity?.scopes) && identity.scopes.every(s => {
|
|
145
|
+
return typeof s === "string";
|
|
146
|
+
})) scopeString = identity.scopes.join(" ");
|
|
147
|
+
if (scopeString === null && required.length > 0) throw new Error(`verifyToken returned no scope/scopes but requiredScopes is set (${required.join(", ")}). Return either scope: string or scopes: string[] from verifyToken.`);
|
|
148
|
+
const tokenScopes = scopeString ? scopeString.split(" ") : [];
|
|
141
149
|
if (required.filter(s => {
|
|
142
150
|
return !tokenScopes.includes(s);
|
|
143
151
|
}).length > 0) throw new Error(`Insufficient scopes. Required: ${required.join(", ")}`);
|
|
@@ -182,6 +190,7 @@ var checkScopes = required => {
|
|
|
182
190
|
var createMcpRouter = (server, options = {}) => {
|
|
183
191
|
const {
|
|
184
192
|
path = "/mcp",
|
|
193
|
+
aliases = [],
|
|
185
194
|
sessionIdGenerator,
|
|
186
195
|
apiBaseUrl,
|
|
187
196
|
getApiHeaders,
|
|
@@ -206,6 +215,7 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
206
215
|
return result;
|
|
207
216
|
};
|
|
208
217
|
const router = new _ttoss_http_server.Router();
|
|
218
|
+
let authCheck;
|
|
209
219
|
if (auth) {
|
|
210
220
|
const verifyToken = buildVerifyToken(auth);
|
|
211
221
|
const publicMethods = new Set(auth.publicMethods ?? DEFAULT_PUBLIC_METHODS);
|
|
@@ -219,20 +229,24 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
219
229
|
},
|
|
220
230
|
resourceMetadataUrl: auth.resourceMetadataUrl
|
|
221
231
|
});
|
|
222
|
-
|
|
232
|
+
authCheck = async (ctx, next) => {
|
|
223
233
|
const method = ctx.request.body?.method;
|
|
224
234
|
if (publicMethods.has(method ?? "")) {
|
|
225
235
|
await next();
|
|
226
236
|
return;
|
|
227
237
|
}
|
|
228
238
|
await verify(ctx, next);
|
|
229
|
-
}
|
|
230
|
-
if (auth.resourceServerUrl && auth.authorizationServerUrl)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
+
}
|
|
236
250
|
}
|
|
237
251
|
const handleWithContext = async (ctx, body) => {
|
|
238
252
|
const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
|
|
@@ -268,7 +282,7 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
268
282
|
}
|
|
269
283
|
});
|
|
270
284
|
};
|
|
271
|
-
|
|
285
|
+
const postHandler = async ctx => {
|
|
272
286
|
try {
|
|
273
287
|
await handleWithContext(ctx, ctx.request.body);
|
|
274
288
|
} catch (error) {
|
|
@@ -279,8 +293,8 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
279
293
|
};
|
|
280
294
|
}
|
|
281
295
|
}
|
|
282
|
-
}
|
|
283
|
-
|
|
296
|
+
};
|
|
297
|
+
const deleteHandler = async ctx => {
|
|
284
298
|
try {
|
|
285
299
|
await handleWithContext(ctx);
|
|
286
300
|
} catch (error) {
|
|
@@ -291,7 +305,15 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
291
305
|
};
|
|
292
306
|
}
|
|
293
307
|
}
|
|
294
|
-
}
|
|
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
|
+
}
|
|
295
317
|
return router;
|
|
296
318
|
};
|
|
297
319
|
var rawSchemaRegistryMap = /* @__PURE__ */new WeakMap();
|
package/dist/index.d.cts
CHANGED
|
@@ -692,7 +692,11 @@ interface McpAuthOptions {
|
|
|
692
692
|
* JWTs, opaque tokens). Resolve with the verified payload, or throw to reject.
|
|
693
693
|
*/
|
|
694
694
|
verifyToken?: (token: string) => Promise<unknown>;
|
|
695
|
-
/**
|
|
695
|
+
/**
|
|
696
|
+
* Scopes that must all be present on the token, else `403`.
|
|
697
|
+
* `verifyToken` may return either `scope: string` (space-separated) or
|
|
698
|
+
* `scopes: string[]`; both are normalised internally.
|
|
699
|
+
*/
|
|
696
700
|
requiredScopes?: string[];
|
|
697
701
|
/**
|
|
698
702
|
* JSON-RPC methods (read from `body.method`) that bypass verification.
|
|
@@ -793,7 +797,9 @@ declare const getIdentity: () => unknown;
|
|
|
793
797
|
* Throws if any scope is missing — the MCP SDK catches this and returns a
|
|
794
798
|
* tool error to the client. Use inside tool handlers for per-tool authorization.
|
|
795
799
|
*
|
|
796
|
-
*
|
|
800
|
+
* Accepts either `scope: string` (space-separated, standard JWT claim) or
|
|
801
|
+
* `scopes: string[]` from `verifyToken`. If neither is present and `required`
|
|
802
|
+
* is non-empty, throws with a descriptive message instead of a silent 403.
|
|
797
803
|
*
|
|
798
804
|
* @example
|
|
799
805
|
* ```typescript
|
|
@@ -813,6 +819,17 @@ interface McpRouterOptions {
|
|
|
813
819
|
* @default '/mcp'
|
|
814
820
|
*/
|
|
815
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[];
|
|
816
833
|
/**
|
|
817
834
|
* Optional session ID generator for stateful MCP servers.
|
|
818
835
|
* When provided, a single shared transport is created and sessions are tracked.
|
package/dist/index.d.mts
CHANGED
|
@@ -692,7 +692,11 @@ interface McpAuthOptions {
|
|
|
692
692
|
* JWTs, opaque tokens). Resolve with the verified payload, or throw to reject.
|
|
693
693
|
*/
|
|
694
694
|
verifyToken?: (token: string) => Promise<unknown>;
|
|
695
|
-
/**
|
|
695
|
+
/**
|
|
696
|
+
* Scopes that must all be present on the token, else `403`.
|
|
697
|
+
* `verifyToken` may return either `scope: string` (space-separated) or
|
|
698
|
+
* `scopes: string[]`; both are normalised internally.
|
|
699
|
+
*/
|
|
696
700
|
requiredScopes?: string[];
|
|
697
701
|
/**
|
|
698
702
|
* JSON-RPC methods (read from `body.method`) that bypass verification.
|
|
@@ -793,7 +797,9 @@ declare const getIdentity: () => unknown;
|
|
|
793
797
|
* Throws if any scope is missing — the MCP SDK catches this and returns a
|
|
794
798
|
* tool error to the client. Use inside tool handlers for per-tool authorization.
|
|
795
799
|
*
|
|
796
|
-
*
|
|
800
|
+
* Accepts either `scope: string` (space-separated, standard JWT claim) or
|
|
801
|
+
* `scopes: string[]` from `verifyToken`. If neither is present and `required`
|
|
802
|
+
* is non-empty, throws with a descriptive message instead of a silent 403.
|
|
797
803
|
*
|
|
798
804
|
* @example
|
|
799
805
|
* ```typescript
|
|
@@ -813,6 +819,17 @@ interface McpRouterOptions {
|
|
|
813
819
|
* @default '/mcp'
|
|
814
820
|
*/
|
|
815
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[];
|
|
816
833
|
/**
|
|
817
834
|
* Optional session ID generator for stateful MCP servers.
|
|
818
835
|
* When provided, a single shared transport is created and sessions are tracked.
|
package/dist/index.mjs
CHANGED
|
@@ -123,7 +123,9 @@ var getIdentity = () => {
|
|
|
123
123
|
* Throws if any scope is missing — the MCP SDK catches this and returns a
|
|
124
124
|
* tool error to the client. Use inside tool handlers for per-tool authorization.
|
|
125
125
|
*
|
|
126
|
-
*
|
|
126
|
+
* Accepts either `scope: string` (space-separated, standard JWT claim) or
|
|
127
|
+
* `scopes: string[]` from `verifyToken`. If neither is present and `required`
|
|
128
|
+
* is non-empty, throws with a descriptive message instead of a silent 403.
|
|
127
129
|
*
|
|
128
130
|
* @example
|
|
129
131
|
* ```typescript
|
|
@@ -134,7 +136,13 @@ var getIdentity = () => {
|
|
|
134
136
|
* ```
|
|
135
137
|
*/
|
|
136
138
|
var checkScopes = required => {
|
|
137
|
-
const
|
|
139
|
+
const identity = getIdentity();
|
|
140
|
+
let scopeString = null;
|
|
141
|
+
if (typeof identity?.scope === "string") scopeString = identity.scope;else if (Array.isArray(identity?.scopes) && identity.scopes.every(s => {
|
|
142
|
+
return typeof s === "string";
|
|
143
|
+
})) scopeString = identity.scopes.join(" ");
|
|
144
|
+
if (scopeString === null && required.length > 0) throw new Error(`verifyToken returned no scope/scopes but requiredScopes is set (${required.join(", ")}). Return either scope: string or scopes: string[] from verifyToken.`);
|
|
145
|
+
const tokenScopes = scopeString ? scopeString.split(" ") : [];
|
|
138
146
|
if (required.filter(s => {
|
|
139
147
|
return !tokenScopes.includes(s);
|
|
140
148
|
}).length > 0) throw new Error(`Insufficient scopes. Required: ${required.join(", ")}`);
|
|
@@ -179,6 +187,7 @@ var checkScopes = required => {
|
|
|
179
187
|
var createMcpRouter = (server, options = {}) => {
|
|
180
188
|
const {
|
|
181
189
|
path = "/mcp",
|
|
190
|
+
aliases = [],
|
|
182
191
|
sessionIdGenerator,
|
|
183
192
|
apiBaseUrl,
|
|
184
193
|
getApiHeaders,
|
|
@@ -203,6 +212,7 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
203
212
|
return result;
|
|
204
213
|
};
|
|
205
214
|
const router = new Router();
|
|
215
|
+
let authCheck;
|
|
206
216
|
if (auth) {
|
|
207
217
|
const verifyToken = buildVerifyToken(auth);
|
|
208
218
|
const publicMethods = new Set(auth.publicMethods ?? DEFAULT_PUBLIC_METHODS);
|
|
@@ -216,20 +226,24 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
216
226
|
},
|
|
217
227
|
resourceMetadataUrl: auth.resourceMetadataUrl
|
|
218
228
|
});
|
|
219
|
-
|
|
229
|
+
authCheck = async (ctx, next) => {
|
|
220
230
|
const method = ctx.request.body?.method;
|
|
221
231
|
if (publicMethods.has(method ?? "")) {
|
|
222
232
|
await next();
|
|
223
233
|
return;
|
|
224
234
|
}
|
|
225
235
|
await verify(ctx, next);
|
|
226
|
-
}
|
|
227
|
-
if (auth.resourceServerUrl && auth.authorizationServerUrl)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
+
}
|
|
233
247
|
}
|
|
234
248
|
const handleWithContext = async (ctx, body) => {
|
|
235
249
|
const apiHeaders = getApiHeaders ? getApiHeaders(ctx) : {};
|
|
@@ -265,7 +279,7 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
265
279
|
}
|
|
266
280
|
});
|
|
267
281
|
};
|
|
268
|
-
|
|
282
|
+
const postHandler = async ctx => {
|
|
269
283
|
try {
|
|
270
284
|
await handleWithContext(ctx, ctx.request.body);
|
|
271
285
|
} catch (error) {
|
|
@@ -276,8 +290,8 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
276
290
|
};
|
|
277
291
|
}
|
|
278
292
|
}
|
|
279
|
-
}
|
|
280
|
-
|
|
293
|
+
};
|
|
294
|
+
const deleteHandler = async ctx => {
|
|
281
295
|
try {
|
|
282
296
|
await handleWithContext(ctx);
|
|
283
297
|
} catch (error) {
|
|
@@ -288,7 +302,15 @@ var createMcpRouter = (server, options = {}) => {
|
|
|
288
302
|
};
|
|
289
303
|
}
|
|
290
304
|
}
|
|
291
|
-
}
|
|
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
|
+
}
|
|
292
314
|
return router;
|
|
293
315
|
};
|
|
294
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.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
37
|
"zod": "^4.4.3",
|
|
38
38
|
"@ttoss/auth-core": "^0.7.0",
|
|
39
|
-
"@ttoss/http-server": "^0.
|
|
40
|
-
"@ttoss/http-server
|
|
39
|
+
"@ttoss/http-server-auth": "^0.3.4",
|
|
40
|
+
"@ttoss/http-server": "^0.7.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/koa": "^3.0.3",
|