@webpieces/mcp-server 0.4.990 → 0.4.992
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 +199 -42
- package/package.json +7 -6
- package/src/McpApiDispatcher.d.ts +6 -3
- package/src/McpApiDispatcher.js +6 -2
- package/src/McpApiDispatcher.js.map +1 -1
- package/src/McpAuth.d.ts +115 -56
- package/src/McpAuth.js +180 -111
- package/src/McpAuth.js.map +1 -1
- package/src/McpAuthorityCompileAssertions.d.ts +12 -2
- package/src/McpAuthorityCompileAssertions.js +32 -4
- package/src/McpAuthorityCompileAssertions.js.map +1 -1
- package/src/McpBindOptions.d.ts +5 -20
- package/src/McpBindOptions.js +5 -15
- package/src/McpBindOptions.js.map +1 -1
- package/src/McpClientSet.d.ts +35 -0
- package/src/McpClientSet.js +86 -0
- package/src/McpClientSet.js.map +1 -0
- package/src/McpDoorAuth.d.ts +117 -0
- package/src/McpDoorAuth.js +455 -0
- package/src/McpDoorAuth.js.map +1 -0
- package/src/McpToolCallRendering.d.ts +5 -4
- package/src/McpToolCallRendering.js.map +1 -1
- package/src/McpToolCatalog.d.ts +1 -1
- package/src/McpToolCatalog.js +1 -1
- package/src/McpToolCatalog.js.map +1 -1
- package/src/McpToolRegistry.js +8 -2
- package/src/McpToolRegistry.js.map +1 -1
- package/src/McpToolset.d.ts +115 -0
- package/src/McpToolset.js +323 -0
- package/src/McpToolset.js.map +1 -0
- package/src/WpMcpErrorTranslator.d.ts +5 -7
- package/src/WpMcpErrorTranslator.js +7 -13
- package/src/WpMcpErrorTranslator.js.map +1 -1
- package/src/WpMcpInMemory.d.ts +30 -0
- package/src/WpMcpInMemory.js +97 -0
- package/src/WpMcpInMemory.js.map +1 -0
- package/src/WpMcpRemote.d.ts +23 -0
- package/src/WpMcpRemote.js +89 -0
- package/src/WpMcpRemote.js.map +1 -0
- package/src/WpMcpServer.d.ts +45 -76
- package/src/WpMcpServer.js +101 -327
- package/src/WpMcpServer.js.map +1 -1
- package/src/__tests__/McpTransportFixtures.d.ts +107 -0
- package/src/__tests__/McpTransportFixtures.js +266 -0
- package/src/__tests__/McpTransportFixtures.js.map +1 -0
- package/src/index.d.ts +9 -2
- package/src/index.js +17 -1
- package/src/index.js.map +1 -1
package/src/McpAuth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WpMcpServerConfig = exports.McpProtectedResourceMetadata = exports.VerifiedMcpCredential = exports.MintedMcpAccessToken = exports.MAX_MCP_SERVER_DESCRIPTION_LENGTH = exports.MAX_MCP_SERVER_TITLE_LENGTH = exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS = exports.MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS = void 0;
|
|
4
|
-
|
|
3
|
+
exports.McpOAuthMode = exports.McpAccessTokenFormat = exports.JWT_PREFIX = exports.DEV_OIDC_TOKEN_PREFIX = exports.GOOGLE_OIDC_ISSUERS = exports.WpMcpServerConfig = exports.McpProtectedResourceMetadata = exports.VerifiedMcpCredential = exports.MintedMcpAccessToken = exports.MAX_MCP_SERVER_DESCRIPTION_LENGTH = exports.MAX_MCP_SERVER_TITLE_LENGTH = exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS = exports.MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS = void 0;
|
|
4
|
+
exports.mcpOAuth = mcpOAuth;
|
|
5
5
|
const McpIcon_1 = require("./McpIcon");
|
|
6
6
|
exports.MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
|
|
7
7
|
exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS = 60 * 60;
|
|
@@ -83,41 +83,44 @@ class McpProtectedResourceMetadata {
|
|
|
83
83
|
}
|
|
84
84
|
exports.McpProtectedResourceMetadata = McpProtectedResourceMetadata;
|
|
85
85
|
/**
|
|
86
|
-
* Node MCP server configuration, built with fluent setters: every name sits next to its own
|
|
87
|
-
* no two settings can be swapped, and a future setting is an additive setter rather than a
|
|
88
|
-
* signature change.
|
|
89
|
-
* is shared with receiving endpoints and document projection.
|
|
86
|
+
* Node MCP HTTP server configuration, built with fluent setters: every name sits next to its own
|
|
87
|
+
* value, so no two settings can be swapped, and a future setting is an additive setter rather than a
|
|
88
|
+
* breaking signature change.
|
|
90
89
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
90
|
+
* WHICH tools the server serves is not here: that is the `McpToolset` handed to
|
|
91
|
+
* `new WpMcpServer(config, toolset)`. This config says only who the server is and HOW A CALLER GETS
|
|
92
|
+
* IN — {@link setAuth}, an OR-set of the same `@WpAuth` builders an endpoint uses plus `mcpOAuth(...)`.
|
|
93
|
+
*
|
|
94
|
+
* SINGLE CANONICAL ORIGIN: one server serves exactly one resource. {@link setResource} is fixed for
|
|
95
|
+
* the lifetime of the process, is the audience of every `mcpOAuth(...)` access token, and is never
|
|
96
|
+
* derived from the request `Host` — see that setter for why a host-derived audience is a confused
|
|
97
|
+
* deputy.
|
|
94
98
|
*
|
|
95
99
|
* Each setter validates its own value immediately and names itself in the failure. Setters cannot
|
|
96
100
|
* make a field impossible to forget the way a positional constructor could, so
|
|
97
101
|
* {@link WpMcpServerConfig.validate} runs from `WpMcpServer.bind(...)` and fails at startup listing
|
|
98
102
|
* EVERY missing required setter at once. A misconfiguration is therefore a boot failure naming the
|
|
99
|
-
* setter, instead of a first-request 401
|
|
100
|
-
* so the client's correct response is to re-authenticate and fail again, forever.
|
|
103
|
+
* setter, instead of a first-request 401 — which, with `mcpOAuth(...)`, is the OAuth discovery
|
|
104
|
+
* signal, so the client's correct response is to re-authenticate and fail again, forever.
|
|
101
105
|
*
|
|
102
106
|
* ```ts
|
|
103
|
-
* const config = new WpMcpServerConfig
|
|
107
|
+
* const config = new WpMcpServerConfig()
|
|
104
108
|
* .setName('my-server')
|
|
105
109
|
* .setTitle('My Product')
|
|
106
110
|
* .setDescription('Search and manage My Product accounts from any MCP client.')
|
|
107
111
|
* .setWebsiteUrl('https://www.example.com/my-product')
|
|
108
112
|
* .setVersion('1.0.0')
|
|
109
|
-
* .setResource('https://api.example.com/mcp')
|
|
110
|
-
* .
|
|
111
|
-
* .
|
|
112
|
-
*
|
|
113
|
-
*
|
|
113
|
+
* .setResource('https://api.example.com/mcp/user')
|
|
114
|
+
* .setAuth([oidcSameProject()]) // internal svc-to-svc MCP
|
|
115
|
+
* .setCredentialVerifiers(router.credentialVerifiers());
|
|
116
|
+
* // .setAuth([oidcSameProject(), jwt(), apiKey('partner', creds),
|
|
117
|
+
* // mcpOAuth(authority, ['https://login.example.com'], ['tools'], McpAccessTokenFormat.opaquePrefix('wpmcp_'))])
|
|
114
118
|
*
|
|
115
119
|
* // the tools/call error translator is a PROCESS-GLOBAL, not a member of this config:
|
|
116
120
|
* McpRegistry.setErrorTranslator(new MyMcpErrorTranslator());
|
|
117
121
|
* ```
|
|
118
122
|
*/
|
|
119
123
|
class WpMcpServerConfig {
|
|
120
|
-
toolProfilesValue = Object.freeze([core_util_1.Mcp.DEFAULT]);
|
|
121
124
|
nameValue;
|
|
122
125
|
titleValue;
|
|
123
126
|
descriptionValue;
|
|
@@ -125,19 +128,8 @@ class WpMcpServerConfig {
|
|
|
125
128
|
versionValue;
|
|
126
129
|
iconsValue;
|
|
127
130
|
resourceValue;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
authorizationServersValue;
|
|
131
|
-
requiredScopesValue;
|
|
132
|
-
maxAccountValidationAgeSecondsValue = exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS;
|
|
133
|
-
/** OPTIONAL. Union of groups exposed by this instance; defaults to [Mcp.DEFAULT]. */
|
|
134
|
-
setToolProfiles(profiles) {
|
|
135
|
-
this.toolProfilesValue = core_util_1.Mcp.profiles(profiles, 'WpMcpServerConfig.setToolProfiles');
|
|
136
|
-
return this;
|
|
137
|
-
}
|
|
138
|
-
get toolProfiles() {
|
|
139
|
-
return this.toolProfilesValue;
|
|
140
|
-
}
|
|
131
|
+
authValue;
|
|
132
|
+
credentialVerifiersValue;
|
|
141
133
|
/** REQUIRED. The MCP server name reported by `initialize`. */
|
|
142
134
|
setName(name) {
|
|
143
135
|
this.nameValue = this.requireText(name, 'setName');
|
|
@@ -199,9 +191,9 @@ class WpMcpServerConfig {
|
|
|
199
191
|
return this.iconsValue;
|
|
200
192
|
}
|
|
201
193
|
/**
|
|
202
|
-
* REQUIRED. The ONE canonical
|
|
203
|
-
*
|
|
204
|
-
* derived from it for the 401 challenge, so a non-URL here is always a mistake — including a
|
|
194
|
+
* REQUIRED. The ONE canonical resource URI, which must be an absolute URL: it is the exact
|
|
195
|
+
* audience every `mcpOAuth(...)` access token is checked against, and {@link resourceMetadataUrl}
|
|
196
|
+
* is derived from it for the 401 challenge, so a non-URL here is always a mistake — including a
|
|
205
197
|
* name or version landed on it.
|
|
206
198
|
*
|
|
207
199
|
* It is fixed for the lifetime of the process and must NEVER be derived from the request `Host`
|
|
@@ -215,45 +207,33 @@ class WpMcpServerConfig {
|
|
|
215
207
|
this.resourceValue = this.requireUrl(resource, 'setResource');
|
|
216
208
|
return this;
|
|
217
209
|
}
|
|
218
|
-
/** REQUIRED. Application-owned paired mint/verify authority for MCP access tokens. */
|
|
219
|
-
setAccessTokenAuthority(authority) {
|
|
220
|
-
this.accessTokenAuthorityValue = this.requirePresent(authority, 'setAccessTokenAuthority');
|
|
221
|
-
return this;
|
|
222
|
-
}
|
|
223
|
-
/** REQUIRED. The common authorization service for tool projection and invocation. */
|
|
224
|
-
setAuthorizationService(authority) {
|
|
225
|
-
this.authorizationServiceValue = this.requirePresent(authority, 'setAuthorizationService');
|
|
226
|
-
return this;
|
|
227
|
-
}
|
|
228
210
|
/**
|
|
229
|
-
* REQUIRED.
|
|
230
|
-
*
|
|
231
|
-
*
|
|
211
|
+
* REQUIRED. HOW a caller gets in: an OR-set of the `@WpAuth` builders endpoints already use —
|
|
212
|
+
* `oidcSameProject()`, `oidcSvcs([...])`, `jwt()`, `apiKey(regime, creds)`, `sharedSecret(key)` —
|
|
213
|
+
* plus `mcpOAuth(...)` for off-the-shelf connectors. Each request is CLASSIFIED from unverified
|
|
214
|
+
* surface facts (which header; a JWT's unverified `iss`) and handed to exactly ONE verifier, whose
|
|
215
|
+
* rejection is the answer — it never falls through to another mode.
|
|
216
|
+
*
|
|
217
|
+
* `vendorAuth(...)` and `@WpAuthPublic` identify no caller to authorize tools against, so they are
|
|
218
|
+
* refused at bind; so is a set whose modes could both claim one request.
|
|
232
219
|
*/
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
throw new Error('WpMcpServerConfig.setAuthorizationServers(...) requires at least one issuer URL.');
|
|
220
|
+
setAuth(auth) {
|
|
221
|
+
if (!Array.isArray(auth) || auth.length === 0) {
|
|
222
|
+
throw new Error('WpMcpServerConfig.setAuth(...) requires at least one auth mode.');
|
|
237
223
|
}
|
|
238
|
-
|
|
239
|
-
this.requireUrl(server, 'setAuthorizationServers');
|
|
240
|
-
this.authorizationServersValue = servers;
|
|
224
|
+
this.authValue = Object.freeze([...auth]);
|
|
241
225
|
return this;
|
|
242
226
|
}
|
|
243
227
|
/**
|
|
244
|
-
* REQUIRED
|
|
245
|
-
*
|
|
228
|
+
* REQUIRED when {@link setAuth} holds any `@WpAuth` mode (everything but `mcpOAuth(...)`): the
|
|
229
|
+
* verifiers this app bound, from `router.credentialVerifiers()` — the SAME JwtHook, OidcHook /
|
|
230
|
+
* DefaultOidcVerifier, AuthConfig and ApiKeyHook the endpoint `AuthFilter` uses.
|
|
246
231
|
*/
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
this.
|
|
252
|
-
return this;
|
|
253
|
-
}
|
|
254
|
-
/** OPTIONAL ceiling, 1 second to 1 hour. Defaults to 1 hour. */
|
|
255
|
-
setMaxAccountValidationAgeSeconds(seconds) {
|
|
256
|
-
this.maxAccountValidationAgeSecondsValue = this.requireSeconds(seconds, exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS, 'setMaxAccountValidationAgeSeconds');
|
|
232
|
+
setCredentialVerifiers(verifiers) {
|
|
233
|
+
if (!verifiers || typeof verifiers.verifyOidc !== 'function') {
|
|
234
|
+
throw new Error('WpMcpServerConfig.setCredentialVerifiers(...) requires router.credentialVerifiers().');
|
|
235
|
+
}
|
|
236
|
+
this.credentialVerifiersValue = verifiers;
|
|
257
237
|
return this;
|
|
258
238
|
}
|
|
259
239
|
/**
|
|
@@ -274,17 +254,12 @@ class WpMcpServerConfig {
|
|
|
274
254
|
missing.push('setVersion(...)');
|
|
275
255
|
if (this.resourceValue === undefined)
|
|
276
256
|
missing.push('setResource(...)');
|
|
277
|
-
if (this.
|
|
278
|
-
missing.push('
|
|
279
|
-
|
|
280
|
-
if (this.
|
|
281
|
-
missing.push('
|
|
257
|
+
if (this.authValue === undefined)
|
|
258
|
+
missing.push('setAuth([...])');
|
|
259
|
+
const needsVerifiers = (this.authValue ?? []).some((mode) => mode.kind !== 'mcp-oauth');
|
|
260
|
+
if (needsVerifiers && this.credentialVerifiersValue === undefined) {
|
|
261
|
+
missing.push('setCredentialVerifiers(router.credentialVerifiers())');
|
|
282
262
|
}
|
|
283
|
-
if (this.authorizationServersValue === undefined) {
|
|
284
|
-
missing.push('setAuthorizationServers(...)');
|
|
285
|
-
}
|
|
286
|
-
if (this.requiredScopesValue === undefined)
|
|
287
|
-
missing.push('setRequiredScopes(...)');
|
|
288
263
|
if (missing.length > 0) {
|
|
289
264
|
throw new Error(`WpMcpServerConfig is missing ${missing.join(', ')}`);
|
|
290
265
|
}
|
|
@@ -313,20 +288,12 @@ class WpMcpServerConfig {
|
|
|
313
288
|
get resource() {
|
|
314
289
|
return this.read(this.resourceValue, 'setResource');
|
|
315
290
|
}
|
|
316
|
-
get
|
|
317
|
-
return this.read(this.
|
|
318
|
-
}
|
|
319
|
-
get authorizationService() {
|
|
320
|
-
return this.read(this.authorizationServiceValue, 'setAuthorizationService');
|
|
321
|
-
}
|
|
322
|
-
get authorizationServers() {
|
|
323
|
-
return this.read(this.authorizationServersValue, 'setAuthorizationServers');
|
|
324
|
-
}
|
|
325
|
-
get requiredScopes() {
|
|
326
|
-
return this.read(this.requiredScopesValue, 'setRequiredScopes');
|
|
291
|
+
get auth() {
|
|
292
|
+
return this.read(this.authValue, 'setAuth');
|
|
327
293
|
}
|
|
328
|
-
|
|
329
|
-
|
|
294
|
+
/** The bound verifiers, or `undefined` for an `mcpOAuth(...)`-only server, which needs none. */
|
|
295
|
+
get credentialVerifiers() {
|
|
296
|
+
return this.credentialVerifiersValue;
|
|
330
297
|
}
|
|
331
298
|
/**
|
|
332
299
|
* RFC 9728 §3.1: the URL of this resource's protected-resource METADATA DOCUMENT, derived from
|
|
@@ -334,17 +301,15 @@ class WpMcpServerConfig {
|
|
|
334
301
|
* (`https://host/mcp` → `https://host/.well-known/oauth-protected-resource/mcp`).
|
|
335
302
|
*
|
|
336
303
|
* This — never the resource identifier itself — is what RFC 9728 §5.1 defines `resource_metadata`
|
|
337
|
-
* to be, so it is the value webpieces puts in the 401 `WWW-Authenticate`
|
|
338
|
-
* mounts no `.well-known` route; publish
|
|
304
|
+
* to be, so it is the value webpieces puts in the `mcpOAuth(...)` 401 `WWW-Authenticate`
|
|
305
|
+
* challenge. webpieces mounts no `.well-known` route; publish
|
|
306
|
+
* `WpMcpServer.protectedResourceMetadata()` at this URL.
|
|
339
307
|
*/
|
|
340
308
|
get resourceMetadataUrl() {
|
|
341
309
|
const url = new URL(this.resource);
|
|
342
310
|
const path = url.pathname === '/' ? '' : url.pathname;
|
|
343
311
|
return `${url.origin}/.well-known/oauth-protected-resource${path}`;
|
|
344
312
|
}
|
|
345
|
-
protectedResourceMetadata() {
|
|
346
|
-
return new McpProtectedResourceMetadata(this.resource, this.authorizationServers, this.requiredScopes);
|
|
347
|
-
}
|
|
348
313
|
/** Copies caller-owned descriptors and nested size arrays before freezing the snapshot. */
|
|
349
314
|
snapshotIcons(icons) {
|
|
350
315
|
if (!Array.isArray(icons)) {
|
|
@@ -376,30 +341,134 @@ class WpMcpServerConfig {
|
|
|
376
341
|
}
|
|
377
342
|
return value;
|
|
378
343
|
}
|
|
379
|
-
|
|
380
|
-
if (
|
|
381
|
-
throw new Error(`WpMcpServerConfig
|
|
344
|
+
read(value, setter) {
|
|
345
|
+
if (value === undefined) {
|
|
346
|
+
throw new Error(`WpMcpServerConfig is missing ${setter}(...)`);
|
|
382
347
|
}
|
|
383
348
|
return value;
|
|
384
349
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
350
|
+
}
|
|
351
|
+
exports.WpMcpServerConfig = WpMcpServerConfig;
|
|
352
|
+
/** Google's OIDC issuers. An `mcpOAuth(...)` issuer may never be one: it would claim Google tokens. */
|
|
353
|
+
exports.GOOGLE_OIDC_ISSUERS = Object.freeze([
|
|
354
|
+
'https://accounts.google.com',
|
|
355
|
+
'accounts.google.com',
|
|
356
|
+
]);
|
|
357
|
+
/** The unsigned off-GCP OIDC token prefix `@webpieces/gcp-identity` mints and verifies. */
|
|
358
|
+
exports.DEV_OIDC_TOKEN_PREFIX = 'dev-oidc.';
|
|
359
|
+
/** Every JWT starts with the base64url of `{"`. An opaque prefix starting so would claim JWTs. */
|
|
360
|
+
exports.JWT_PREFIX = 'eyJ';
|
|
361
|
+
/**
|
|
362
|
+
* How an `mcpOAuth(...)` access token LOOKS, decided by the authority that mints it. The MCP door
|
|
363
|
+
* classifies every request from unverified surface facts before handing it to exactly ONE verifier, so
|
|
364
|
+
* the OAuth mode must say which bearers are its kind without any crypto or I/O:
|
|
365
|
+
*
|
|
366
|
+
* - {@link opaquePrefix} — the authority mints opaque tokens that all start with `prefix`
|
|
367
|
+
* (e.g. `'wpmcp_'`). Disjoint from every JWT and from off-GCP OIDC tokens by construction.
|
|
368
|
+
* - {@link jwtIssuer} — the authority mints JWTs whose `iss` is one of the mode's
|
|
369
|
+
* `authorizationServers`. A JWT with any other issuer is not this mode's.
|
|
370
|
+
* - {@link everyBearer} — every `Authorization: Bearer` credential is this mode's. Legal ONLY when no
|
|
371
|
+
* other bearer mode (`oidcSameProject()`, `oidcSvcs(...)`, `jwt()`, a bearer `apiKey(...)`) is in the
|
|
372
|
+
* same `setAuth([...])`; startup refuses the combination, because two modes claiming one token is
|
|
373
|
+
* exactly the overlap classify-then-verify forbids.
|
|
374
|
+
*/
|
|
375
|
+
class McpAccessTokenFormat {
|
|
376
|
+
kind;
|
|
377
|
+
prefix;
|
|
378
|
+
constructor(kind, prefix) {
|
|
379
|
+
this.kind = kind;
|
|
380
|
+
this.prefix = prefix;
|
|
381
|
+
}
|
|
382
|
+
// webpieces-disable no-function-outside-class -- explicit public factory of a closed set of formats
|
|
383
|
+
static opaquePrefix(prefix) {
|
|
384
|
+
if (typeof prefix !== 'string' || prefix.trim() === '' || /\s/.test(prefix)) {
|
|
385
|
+
throw new Error('McpAccessTokenFormat.opaquePrefix(...) requires a nonblank prefix with no whitespace.');
|
|
388
386
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
387
|
+
for (const reserved of [exports.JWT_PREFIX, exports.DEV_OIDC_TOKEN_PREFIX]) {
|
|
388
|
+
if (reserved.startsWith(prefix) || prefix.startsWith(reserved)) {
|
|
389
|
+
throw new Error(`McpAccessTokenFormat.opaquePrefix('${prefix}') overlaps '${reserved}', which ` +
|
|
390
|
+
(reserved === exports.JWT_PREFIX
|
|
391
|
+
? 'every JWT starts with'
|
|
392
|
+
: 'every off-GCP OIDC token starts with') +
|
|
393
|
+
"; pick a prefix the authority alone mints, e.g. 'wpmcp_'.");
|
|
394
|
+
}
|
|
394
395
|
}
|
|
395
|
-
return
|
|
396
|
+
return new McpAccessTokenFormat('opaque-prefix', prefix);
|
|
396
397
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
398
|
+
// webpieces-disable no-function-outside-class -- explicit public factory of a closed set of formats
|
|
399
|
+
static jwtIssuer() {
|
|
400
|
+
return new McpAccessTokenFormat('jwt-issuer', '');
|
|
401
|
+
}
|
|
402
|
+
// webpieces-disable no-function-outside-class -- explicit public factory of a closed set of formats
|
|
403
|
+
static everyBearer() {
|
|
404
|
+
return new McpAccessTokenFormat('every-bearer', '');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
exports.McpAccessTokenFormat = McpAccessTokenFormat;
|
|
408
|
+
/**
|
|
409
|
+
* The MCP OAuth mode: today's connector behavior (Claude / ChatGPT) as ONE member of a server's
|
|
410
|
+
* `setAuth([...])` set. It is the only mode that publishes RFC 9728 protected-resource metadata and
|
|
411
|
+
* answers a request carrying no credential with the `401 WWW-Authenticate` discovery challenge, and
|
|
412
|
+
* its access tokens are audience-bound to the server's canonical resource. Build it with
|
|
413
|
+
* {@link mcpOAuth}.
|
|
414
|
+
*/
|
|
415
|
+
class McpOAuthMode {
|
|
416
|
+
authority;
|
|
417
|
+
authorizationServers;
|
|
418
|
+
requiredScopes;
|
|
419
|
+
tokenFormat;
|
|
420
|
+
maxAccountValidationAgeSeconds;
|
|
421
|
+
kind = 'mcp-oauth';
|
|
422
|
+
constructor(authority, authorizationServers, requiredScopes, tokenFormat, maxAccountValidationAgeSeconds) {
|
|
423
|
+
this.authority = authority;
|
|
424
|
+
this.authorizationServers = authorizationServers;
|
|
425
|
+
this.requiredScopes = requiredScopes;
|
|
426
|
+
this.tokenFormat = tokenFormat;
|
|
427
|
+
this.maxAccountValidationAgeSeconds = maxAccountValidationAgeSeconds;
|
|
428
|
+
if (!authority || typeof authority.verifyAccessToken !== 'function') {
|
|
429
|
+
throw new Error('mcpOAuth(...) requires an McpAccessTokenAuthority.');
|
|
400
430
|
}
|
|
401
|
-
|
|
431
|
+
if (!Array.isArray(authorizationServers) || authorizationServers.length === 0) {
|
|
432
|
+
throw new Error('mcpOAuth(...) requires at least one authorization server issuer URL.');
|
|
433
|
+
}
|
|
434
|
+
for (const server of authorizationServers) {
|
|
435
|
+
if (typeof server !== 'string' || !URL.canParse(server)) {
|
|
436
|
+
throw new Error(`mcpOAuth(...) authorization servers must be absolute URLs, got '${String(server)}'.`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (!Array.isArray(requiredScopes) ||
|
|
440
|
+
!requiredScopes.every((scope) => typeof scope === 'string' && scope.trim() !== '')) {
|
|
441
|
+
throw new Error('mcpOAuth(...) requires an array of nonblank required scopes (empty only by saying so: []).');
|
|
442
|
+
}
|
|
443
|
+
if (!(tokenFormat instanceof McpAccessTokenFormat)) {
|
|
444
|
+
throw new Error('mcpOAuth(...) requires an McpAccessTokenFormat describing the tokens the authority mints.');
|
|
445
|
+
}
|
|
446
|
+
if (!Number.isFinite(maxAccountValidationAgeSeconds) ||
|
|
447
|
+
maxAccountValidationAgeSeconds <= 0 ||
|
|
448
|
+
maxAccountValidationAgeSeconds > exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS) {
|
|
449
|
+
throw new Error(`mcpOAuth(...) maxAccountValidationAgeSeconds must be 1..${exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS}, ` +
|
|
450
|
+
`got ${String(maxAccountValidationAgeSeconds)}.`);
|
|
451
|
+
}
|
|
452
|
+
Object.freeze(this);
|
|
453
|
+
}
|
|
454
|
+
/** RFC 9728 metadata for `resource`, published by the app at the server's `resourceMetadataUrl`. */
|
|
455
|
+
protectedResourceMetadata(resource) {
|
|
456
|
+
return new McpProtectedResourceMetadata(resource, this.authorizationServers, this.requiredScopes);
|
|
402
457
|
}
|
|
403
458
|
}
|
|
404
|
-
exports.
|
|
459
|
+
exports.McpOAuthMode = McpOAuthMode;
|
|
460
|
+
/**
|
|
461
|
+
* The MCP OAuth door mode, for off-the-shelf connectors. Everything OAuth-only lives here and is
|
|
462
|
+
* required only when this mode is in the server's `setAuth([...])`:
|
|
463
|
+
*
|
|
464
|
+
* @param authority the application-owned paired mint/verify authority
|
|
465
|
+
* @param authorizationServers the trusted token issuers, published as `authorization_servers`
|
|
466
|
+
* @param requiredScopes scopes every access token must carry before any tool is dispatched
|
|
467
|
+
* @param tokenFormat how the authority's tokens look, so the door can classify them
|
|
468
|
+
* @param maxAccountValidationAgeSeconds how fresh the account facts must be, 1 second to 1 hour
|
|
469
|
+
*/
|
|
470
|
+
// webpieces-disable no-function-outside-class -- canonical credential descriptor factory, the twin of jwt()/oidcSameProject()
|
|
471
|
+
function mcpOAuth(authority, authorizationServers, requiredScopes, tokenFormat, maxAccountValidationAgeSeconds = exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS) {
|
|
472
|
+
return new McpOAuthMode(authority, authorizationServers, requiredScopes, tokenFormat, maxAccountValidationAgeSeconds);
|
|
473
|
+
}
|
|
405
474
|
//# sourceMappingURL=McpAuth.js.map
|
package/src/McpAuth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpAuth.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpAuth.ts"],"names":[],"mappings":"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","sourcesContent":["import { Mcp, McpToolProfiles } from '@webpieces/core-util';\nimport { AuthenticatedCaller, AuthorizationService } from '@webpieces/http-routing';\nimport { Icon } from '@modelcontextprotocol/server';\nimport { McpIcon } from './McpIcon';\n\nexport const MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS = 30 * 24 * 60 * 60;\nexport const MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS = 60 * 60;\n/** `setTitle` ceiling: the Claude Connectors Directory's listing Name limit. */\nexport const MAX_MCP_SERVER_TITLE_LENGTH = 100;\n/** `setDescription` ceiling: the Claude Connectors Directory's listing Description limit. */\nexport const MAX_MCP_SERVER_DESCRIPTION_LENGTH = 2000;\n\n/** Framework-normalized access-token result. The token representation remains application-owned. */\nexport class MintedMcpAccessToken {\n constructor(\n public readonly token: string,\n public readonly resource: string,\n public readonly issuedAtEpochSeconds: number,\n public readonly expiresAtEpochSeconds: number,\n ) {\n if (token.trim() === '' || resource.trim() === '') {\n throw new Error('Minted MCP access token and resource must be non-empty.');\n }\n if (!Number.isFinite(issuedAtEpochSeconds) || !Number.isFinite(expiresAtEpochSeconds)) {\n throw new Error(\n 'Minted MCP access token timestamps must be finite epoch-second values.',\n );\n }\n if (expiresAtEpochSeconds <= issuedAtEpochSeconds) {\n throw new Error('MCP access token expiry must be after issuance.');\n }\n if (expiresAtEpochSeconds - issuedAtEpochSeconds > MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS) {\n throw new Error('MCP access token lifetime must not exceed 30 days.');\n }\n }\n}\n\n/**\n * Fresh, authoritative security facts and an explicit application-owned caller mapping resolved at the MCP resource boundary. Implementations must\n * verify issuer, exact resource/audience, expiry, token type/version, key/algorithm policy, and\n * current account state. Opaque access tokens are fully supported.\n */\nexport class VerifiedMcpCredential {\n constructor(\n public readonly subject: string,\n public readonly issuer: string,\n public readonly resource: string,\n public readonly issuedAtEpochSeconds: number,\n public readonly expiresAtEpochSeconds: number,\n public readonly scopes: readonly string[],\n /** When current enabled/revoked state and roles were read from the authoritative source. */\n public readonly accountValidatedAtEpochSeconds: number,\n /** Application mapping of freshly verified account facts into canonical trusted context. */\n public readonly caller: AuthenticatedCaller,\n ) {}\n\n /** The canonical application's roles; there is no independent listing-role snapshot. */\n get principalRoles(): readonly string[] {\n return this.caller.roles;\n }\n}\n\n/**\n * Application-owned paired mint/verify authority for resource-bound MCP access tokens.\n * `verifyAccessToken` must throw `ApiUnauthorizedError` for a token it rejects (the bind boundary\n * answers 401 + `WWW-Authenticate`); any other throw is treated as an implementation failure (500).\n */\nexport interface McpAccessTokenAuthority<TGrant> {\n mintAccessToken(grant: TGrant): Promise<MintedMcpAccessToken>;\n verifyAccessToken(\n accessToken: string,\n expectedResource: string,\n ): Promise<VerifiedMcpCredential>;\n}\n\n/** RFC 9728-style metadata exposed by the MCP protected resource. */\nexport class McpProtectedResourceMetadata {\n readonly resource: string;\n readonly authorization_servers: readonly string[];\n readonly bearer_methods_supported = ['header'];\n readonly scopes_supported?: readonly string[];\n\n constructor(\n resource: string,\n authorizationServers: readonly string[],\n scopesSupported?: readonly string[],\n ) {\n this.resource = resource;\n this.authorization_servers = authorizationServers;\n this.scopes_supported = scopesSupported;\n }\n}\n\n/**\n * Node MCP server configuration, built with fluent setters: every name sits next to its own value, so\n * no two settings can be swapped, and a future setting is an additive setter rather than a breaking\n * signature change. The access-token authority verifies MCP credentials; the authorization service\n * is shared with receiving endpoints and document projection.\n *\n * SINGLE CANONICAL ORIGIN: one server serves exactly one protected resource. {@link setResource} is\n * fixed for the lifetime of the process, is the audience of every access token, and is never derived\n * from the request `Host` — see that setter for why a host-derived audience is a confused deputy.\n *\n * Each setter validates its own value immediately and names itself in the failure. Setters cannot\n * make a field impossible to forget the way a positional constructor could, so\n * {@link WpMcpServerConfig.validate} runs from `WpMcpServer.bind(...)` and fails at startup listing\n * EVERY missing required setter at once. A misconfiguration is therefore a boot failure naming the\n * setter, instead of a first-request 401 + `WWW-Authenticate` — which is the OAuth discovery signal,\n * so the client's correct response is to re-authenticate and fail again, forever.\n *\n * ```ts\n * const config = new WpMcpServerConfig<MyGrant>()\n * .setName('my-server')\n * .setTitle('My Product')\n * .setDescription('Search and manage My Product accounts from any MCP client.')\n * .setWebsiteUrl('https://www.example.com/my-product')\n * .setVersion('1.0.0')\n * .setResource('https://api.example.com/mcp')\n * .setAccessTokenAuthority(authority)\n * .setAuthorizationService(router.authorizationService())\n * .setAuthorizationServers(['https://login.example.com'])\n * .setRequiredScopes(['tools']);\n *\n * // the tools/call error translator is a PROCESS-GLOBAL, not a member of this config:\n * McpRegistry.setErrorTranslator(new MyMcpErrorTranslator());\n * ```\n */\nexport class WpMcpServerConfig<TGrant> {\n private toolProfilesValue: readonly string[] = Object.freeze([Mcp.DEFAULT]);\n private nameValue?: string;\n private titleValue?: string;\n private descriptionValue?: string;\n private websiteUrlValue?: string;\n private versionValue?: string;\n private iconsValue?: readonly McpIcon[];\n private resourceValue?: string;\n private accessTokenAuthorityValue?: McpAccessTokenAuthority<TGrant>;\n private authorizationServiceValue?: AuthorizationService;\n private authorizationServersValue?: readonly string[];\n private requiredScopesValue?: readonly string[];\n private maxAccountValidationAgeSecondsValue = MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS;\n\n /** OPTIONAL. Union of groups exposed by this instance; defaults to [Mcp.DEFAULT]. */\n setToolProfiles(profiles: McpToolProfiles): this {\n this.toolProfilesValue = Mcp.profiles(profiles, 'WpMcpServerConfig.setToolProfiles');\n return this;\n }\n\n get toolProfiles(): readonly string[] {\n return this.toolProfilesValue;\n }\n\n /** REQUIRED. The MCP server name reported by `initialize`. */\n setName(name: string): this {\n this.nameValue = this.requireText(name, 'setName');\n return this;\n }\n\n /**\n * REQUIRED. The human-readable display name sent as `serverInfo.title` (MCP 2025-06-18), e.g.\n * `'Lang Learning'` where {@link setName} is the programmatic `'lang-learning'`. One line, at most\n * 100 characters — the Claude Connectors Directory's listing Name limit.\n *\n * The app sets it because the server cannot derive it: it does not read the OpenAPI manifest at\n * runtime, and one MCP server may bind contracts from several api libraries.\n */\n setTitle(title: string): this {\n const value = this.requireText(title, 'setTitle');\n if (/[\\r\\n]/.test(value)) {\n throw new Error('WpMcpServerConfig.setTitle(...) requires a single line of text.');\n }\n this.titleValue = this.requireMaxLength(value, MAX_MCP_SERVER_TITLE_LENGTH, 'setTitle');\n return this;\n }\n\n /**\n * REQUIRED. What this server does, sent as `serverInfo.description` (MCP 2025-11-25). At most\n * 2000 characters — the Claude Connectors Directory's listing Description limit.\n */\n setDescription(description: string): this {\n const value = this.requireText(description, 'setDescription');\n this.descriptionValue = this.requireMaxLength(\n value,\n MAX_MCP_SERVER_DESCRIPTION_LENGTH,\n 'setDescription',\n );\n return this;\n }\n\n /**\n * REQUIRED. An absolute `https://` URL of a page a HUMAN reads about this server — the product\n * homepage or its docs site — sent as `serverInfo.websiteUrl` (MCP 2025-11-25).\n *\n * Deliberately NOT derived from {@link setResource}: that is the MCP endpoint, an API host, not a\n * website, and the resource is never derived from request input either. No default, because the\n * page a user is sent to is the app's decision.\n */\n setWebsiteUrl(websiteUrl: string): this {\n const value = this.requireUrl(websiteUrl, 'setWebsiteUrl');\n if (new URL(value).protocol !== 'https:') {\n throw new Error(\n `WpMcpServerConfig.setWebsiteUrl(...) requires an https:// URL, got '${value}'.`,\n );\n }\n this.websiteUrlValue = value;\n return this;\n }\n\n /** REQUIRED. The MCP server version; the registry revision is appended to it at bind time. */\n setVersion(version: string): this {\n this.versionValue = this.requireText(version, 'setVersion');\n return this;\n }\n\n /** OPTIONAL. Standard MCP icons; copied and frozen, with no network access. */\n setIcons(icons: readonly Icon[]): this {\n this.iconsValue = this.snapshotIcons(icons);\n return this;\n }\n\n /** Immutable snapshot; omitted icons remain absent from SDK implementation metadata. */\n get icons(): readonly McpIcon[] | undefined {\n return this.iconsValue;\n }\n\n /**\n * REQUIRED. The ONE canonical protected-resource URI, which must be an absolute URL: it is the\n * exact audience every access token is checked against, and {@link resourceMetadataUrl} is\n * derived from it for the 401 challenge, so a non-URL here is always a mistake — including a\n * name or version landed on it.\n *\n * It is fixed for the lifetime of the process and must NEVER be derived from the request `Host`\n * header: the boundary compares a token's audience against this value, so a host-derived\n * resource compares two values both taken from caller-controlled input — the confused-deputy\n * shape this check exists to prevent. Its path must equal `McpBindOptions.endpointPath`, which\n * `WpMcpServer.bind(...)` verifies. A second hostname is a second deployment, not a second\n * audience.\n */\n setResource(resource: string): this {\n this.resourceValue = this.requireUrl(resource, 'setResource');\n return this;\n }\n\n /** REQUIRED. Application-owned paired mint/verify authority for MCP access tokens. */\n setAccessTokenAuthority(authority: McpAccessTokenAuthority<TGrant>): this {\n this.accessTokenAuthorityValue = this.requirePresent(authority, 'setAccessTokenAuthority');\n return this;\n }\n\n /** REQUIRED. The common authorization service for tool projection and invocation. */\n setAuthorizationService(authority: AuthorizationService): this {\n this.authorizationServiceValue = this.requirePresent(authority, 'setAuthorizationService');\n return this;\n }\n\n /**\n * REQUIRED. The trusted token issuers. Every entry must be an absolute URL, because each is\n * compared against a token's `issuer` claim and published as an `authorization_servers` entry —\n * which is what makes a scope list landed here fail at boot rather than on the first request.\n */\n setAuthorizationServers(authorizationServers: readonly string[]): this {\n const servers = this.requireArray(authorizationServers, 'setAuthorizationServers');\n if (servers.length === 0) {\n throw new Error(\n 'WpMcpServerConfig.setAuthorizationServers(...) requires at least one issuer URL.',\n );\n }\n for (const server of servers) this.requireUrl(server, 'setAuthorizationServers');\n this.authorizationServersValue = servers;\n return this;\n }\n\n /**\n * REQUIRED, and empty only by saying so explicitly. Scopes every access token must carry before\n * any tool is dispatched.\n */\n setRequiredScopes(requiredScopes: readonly string[]): this {\n const scopes = this.requireArray(requiredScopes, 'setRequiredScopes');\n for (const scope of scopes) this.requireText(scope, 'setRequiredScopes');\n this.requiredScopesValue = scopes;\n return this;\n }\n\n /** OPTIONAL ceiling, 1 second to 1 hour. Defaults to 1 hour. */\n setMaxAccountValidationAgeSeconds(seconds: number): this {\n this.maxAccountValidationAgeSecondsValue = this.requireSeconds(\n seconds,\n MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS,\n 'setMaxAccountValidationAgeSeconds',\n );\n return this;\n }\n\n /**\n * Called by `WpMcpServer.bind(...)`. Throws naming EVERY missing required setter at once, so a\n * half-built config is repaired in one pass rather than one boot failure at a time.\n */\n validate(): void {\n const missing: string[] = [];\n if (this.nameValue === undefined) missing.push('setName(...)');\n if (this.titleValue === undefined) missing.push('setTitle(...)');\n if (this.descriptionValue === undefined) missing.push('setDescription(...)');\n if (this.websiteUrlValue === undefined) missing.push('setWebsiteUrl(...)');\n if (this.versionValue === undefined) missing.push('setVersion(...)');\n if (this.resourceValue === undefined) missing.push('setResource(...)');\n if (this.accessTokenAuthorityValue === undefined) {\n missing.push('setAccessTokenAuthority(...)');\n }\n if (this.authorizationServiceValue === undefined) {\n missing.push('setAuthorizationService(...)');\n }\n if (this.authorizationServersValue === undefined) {\n missing.push('setAuthorizationServers(...)');\n }\n if (this.requiredScopesValue === undefined) missing.push('setRequiredScopes(...)');\n if (missing.length > 0) {\n throw new Error(`WpMcpServerConfig is missing ${missing.join(', ')}`);\n }\n for (const icon of this.iconsValue ?? []) {\n const url = new URL(icon.src);\n if (url.protocol === 'https:' && url.origin !== new URL(this.resource).origin) {\n throw new Error(\n 'WpMcpServerConfig.setIcons(...) requires HTTPS icons on the resource origin.',\n );\n }\n }\n }\n\n get name(): string {\n return this.read(this.nameValue, 'setName');\n }\n\n get title(): string {\n return this.read(this.titleValue, 'setTitle');\n }\n\n get description(): string {\n return this.read(this.descriptionValue, 'setDescription');\n }\n\n get websiteUrl(): string {\n return this.read(this.websiteUrlValue, 'setWebsiteUrl');\n }\n\n get version(): string {\n return this.read(this.versionValue, 'setVersion');\n }\n\n get resource(): string {\n return this.read(this.resourceValue, 'setResource');\n }\n\n get accessTokenAuthority(): McpAccessTokenAuthority<TGrant> {\n return this.read(this.accessTokenAuthorityValue, 'setAccessTokenAuthority');\n }\n\n get authorizationService(): AuthorizationService {\n return this.read(this.authorizationServiceValue, 'setAuthorizationService');\n }\n\n get authorizationServers(): readonly string[] {\n return this.read(this.authorizationServersValue, 'setAuthorizationServers');\n }\n\n get requiredScopes(): readonly string[] {\n return this.read(this.requiredScopesValue, 'setRequiredScopes');\n }\n\n get maxAccountValidationAgeSeconds(): number {\n return this.maxAccountValidationAgeSecondsValue;\n }\n\n /**\n * RFC 9728 §3.1: the URL of this resource's protected-resource METADATA DOCUMENT, derived from\n * {@link resource} by inserting `/.well-known/oauth-protected-resource` between origin and path\n * (`https://host/mcp` → `https://host/.well-known/oauth-protected-resource/mcp`).\n *\n * This — never the resource identifier itself — is what RFC 9728 §5.1 defines `resource_metadata`\n * to be, so it is the value webpieces puts in the 401 `WWW-Authenticate` challenge. webpieces\n * mounts no `.well-known` route; publish {@link protectedResourceMetadata} at this URL.\n */\n get resourceMetadataUrl(): string {\n const url = new URL(this.resource);\n const path = url.pathname === '/' ? '' : url.pathname;\n return `${url.origin}/.well-known/oauth-protected-resource${path}`;\n }\n\n protectedResourceMetadata(): McpProtectedResourceMetadata {\n return new McpProtectedResourceMetadata(\n this.resource,\n this.authorizationServers,\n this.requiredScopes,\n );\n }\n\n /** Copies caller-owned descriptors and nested size arrays before freezing the snapshot. */\n private snapshotIcons(icons: readonly Icon[]): readonly McpIcon[] {\n if (!Array.isArray(icons)) {\n throw new Error(\n 'WpMcpServerConfig.setIcons(...) requires an array of icon descriptors.',\n );\n }\n return Object.freeze(\n icons.map((icon: Icon) => {\n if (!icon || typeof icon !== 'object') {\n throw new Error('WpMcpServerConfig.setIcons(...) requires icon descriptors.');\n }\n return new McpIcon(icon.src, icon.mimeType, icon.sizes, icon.theme);\n }),\n );\n }\n\n private requireText(value: string, setter: string): string {\n if (typeof value !== 'string' || value.trim() === '') {\n throw new Error(`WpMcpServerConfig.${setter}(...) requires a non-empty string.`);\n }\n return value;\n }\n\n private requireUrl(value: string, setter: string): string {\n this.requireText(value, setter);\n if (!URL.canParse(value)) {\n throw new Error(\n `WpMcpServerConfig.${setter}(...) requires an absolute URL, got '${value}'.`,\n );\n }\n return value;\n }\n\n private requireMaxLength(value: string, max: number, setter: string): string {\n if (value.length > max) {\n throw new Error(\n `WpMcpServerConfig.${setter}(...) allows at most ${max} characters, got ${value.length}.`,\n );\n }\n return value;\n }\n\n private requireArray(value: readonly string[], setter: string): readonly string[] {\n if (!Array.isArray(value)) {\n throw new Error(`WpMcpServerConfig.${setter}(...) requires an array of strings.`);\n }\n return value;\n }\n\n private requireSeconds(value: number, ceiling: number, setter: string): number {\n if (!Number.isFinite(value) || value <= 0 || value > ceiling) {\n throw new Error(\n `WpMcpServerConfig.${setter}(...) requires 1..${ceiling} seconds, got ${String(value)}.`,\n );\n }\n return value;\n }\n\n private requirePresent<T>(value: T, setter: string): T {\n if (value === undefined || value === null) {\n throw new Error(`WpMcpServerConfig.${setter}(...) requires a value.`);\n }\n return value;\n }\n\n private read<T>(value: T | undefined, setter: string): T {\n if (value === undefined) {\n throw new Error(`WpMcpServerConfig is missing ${setter}(...)`);\n }\n return value;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"McpAuth.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpAuth.ts"],"names":[],"mappings":"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","sourcesContent":["import { AuthMethod } from '@webpieces/core-util';\nimport { AuthenticatedCaller, CredentialVerifiers } from '@webpieces/http-routing';\nimport { Icon } from '@modelcontextprotocol/server';\nimport { McpIcon } from './McpIcon';\n\nexport const MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS = 30 * 24 * 60 * 60;\nexport const MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS = 60 * 60;\n/** `setTitle` ceiling: the Claude Connectors Directory's listing Name limit. */\nexport const MAX_MCP_SERVER_TITLE_LENGTH = 100;\n/** `setDescription` ceiling: the Claude Connectors Directory's listing Description limit. */\nexport const MAX_MCP_SERVER_DESCRIPTION_LENGTH = 2000;\n\n/** Framework-normalized access-token result. The token representation remains application-owned. */\nexport class MintedMcpAccessToken {\n constructor(\n public readonly token: string,\n public readonly resource: string,\n public readonly issuedAtEpochSeconds: number,\n public readonly expiresAtEpochSeconds: number,\n ) {\n if (token.trim() === '' || resource.trim() === '') {\n throw new Error('Minted MCP access token and resource must be non-empty.');\n }\n if (!Number.isFinite(issuedAtEpochSeconds) || !Number.isFinite(expiresAtEpochSeconds)) {\n throw new Error(\n 'Minted MCP access token timestamps must be finite epoch-second values.',\n );\n }\n if (expiresAtEpochSeconds <= issuedAtEpochSeconds) {\n throw new Error('MCP access token expiry must be after issuance.');\n }\n if (expiresAtEpochSeconds - issuedAtEpochSeconds > MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS) {\n throw new Error('MCP access token lifetime must not exceed 30 days.');\n }\n }\n}\n\n/**\n * Fresh, authoritative security facts and an explicit application-owned caller mapping resolved at the MCP resource boundary. Implementations must\n * verify issuer, exact resource/audience, expiry, token type/version, key/algorithm policy, and\n * current account state. Opaque access tokens are fully supported.\n */\nexport class VerifiedMcpCredential {\n constructor(\n public readonly subject: string,\n public readonly issuer: string,\n public readonly resource: string,\n public readonly issuedAtEpochSeconds: number,\n public readonly expiresAtEpochSeconds: number,\n public readonly scopes: readonly string[],\n /** When current enabled/revoked state and roles were read from the authoritative source. */\n public readonly accountValidatedAtEpochSeconds: number,\n /** Application mapping of freshly verified account facts into canonical trusted context. */\n public readonly caller: AuthenticatedCaller,\n ) {}\n\n /** The canonical application's roles; there is no independent listing-role snapshot. */\n get principalRoles(): readonly string[] {\n return this.caller.roles;\n }\n}\n\n/**\n * Application-owned paired mint/verify authority for resource-bound MCP access tokens, handed to\n * `mcpOAuth(...)`. `verifyAccessToken` must throw `ApiUnauthorizedError` for a token it rejects (the bind boundary\n * answers 401 + `WWW-Authenticate`); any other throw is treated as an implementation failure (500).\n */\nexport interface McpAccessTokenAuthority<TGrant> {\n mintAccessToken(grant: TGrant): Promise<MintedMcpAccessToken>;\n verifyAccessToken(\n accessToken: string,\n expectedResource: string,\n ): Promise<VerifiedMcpCredential>;\n}\n\n/** RFC 9728-style metadata exposed by the MCP protected resource. */\nexport class McpProtectedResourceMetadata {\n readonly resource: string;\n readonly authorization_servers: readonly string[];\n readonly bearer_methods_supported = ['header'];\n readonly scopes_supported?: readonly string[];\n\n constructor(\n resource: string,\n authorizationServers: readonly string[],\n scopesSupported?: readonly string[],\n ) {\n this.resource = resource;\n this.authorization_servers = authorizationServers;\n this.scopes_supported = scopesSupported;\n }\n}\n\n/**\n * One member of a server's `setAuth([...])` set: an `@WpAuth` builder (`oidcSameProject()`,\n * `oidcSvcs([...])`, `jwt()`, `apiKey(...)`, `sharedSecret(...)`) or `mcpOAuth(...)`.\n */\nexport type McpHttpAuth = AuthMethod | McpOAuthMode;\n\n/** A server's `setAuth([...])` set: at least one mode. */\nexport type McpHttpAuths = readonly [McpHttpAuth, ...McpHttpAuth[]];\n\n/**\n * Node MCP HTTP server configuration, built with fluent setters: every name sits next to its own\n * value, so no two settings can be swapped, and a future setting is an additive setter rather than a\n * breaking signature change.\n *\n * WHICH tools the server serves is not here: that is the `McpToolset` handed to\n * `new WpMcpServer(config, toolset)`. This config says only who the server is and HOW A CALLER GETS\n * IN — {@link setAuth}, an OR-set of the same `@WpAuth` builders an endpoint uses plus `mcpOAuth(...)`.\n *\n * SINGLE CANONICAL ORIGIN: one server serves exactly one resource. {@link setResource} is fixed for\n * the lifetime of the process, is the audience of every `mcpOAuth(...)` access token, and is never\n * derived from the request `Host` — see that setter for why a host-derived audience is a confused\n * deputy.\n *\n * Each setter validates its own value immediately and names itself in the failure. Setters cannot\n * make a field impossible to forget the way a positional constructor could, so\n * {@link WpMcpServerConfig.validate} runs from `WpMcpServer.bind(...)` and fails at startup listing\n * EVERY missing required setter at once. A misconfiguration is therefore a boot failure naming the\n * setter, instead of a first-request 401 — which, with `mcpOAuth(...)`, is the OAuth discovery\n * signal, so the client's correct response is to re-authenticate and fail again, forever.\n *\n * ```ts\n * const config = new WpMcpServerConfig()\n * .setName('my-server')\n * .setTitle('My Product')\n * .setDescription('Search and manage My Product accounts from any MCP client.')\n * .setWebsiteUrl('https://www.example.com/my-product')\n * .setVersion('1.0.0')\n * .setResource('https://api.example.com/mcp/user')\n * .setAuth([oidcSameProject()]) // internal svc-to-svc MCP\n * .setCredentialVerifiers(router.credentialVerifiers());\n * // .setAuth([oidcSameProject(), jwt(), apiKey('partner', creds),\n * // mcpOAuth(authority, ['https://login.example.com'], ['tools'], McpAccessTokenFormat.opaquePrefix('wpmcp_'))])\n *\n * // the tools/call error translator is a PROCESS-GLOBAL, not a member of this config:\n * McpRegistry.setErrorTranslator(new MyMcpErrorTranslator());\n * ```\n */\nexport class WpMcpServerConfig {\n private nameValue?: string;\n private titleValue?: string;\n private descriptionValue?: string;\n private websiteUrlValue?: string;\n private versionValue?: string;\n private iconsValue?: readonly McpIcon[];\n private resourceValue?: string;\n private authValue?: McpHttpAuths;\n private credentialVerifiersValue?: CredentialVerifiers;\n\n /** REQUIRED. The MCP server name reported by `initialize`. */\n setName(name: string): this {\n this.nameValue = this.requireText(name, 'setName');\n return this;\n }\n\n /**\n * REQUIRED. The human-readable display name sent as `serverInfo.title` (MCP 2025-06-18), e.g.\n * `'Lang Learning'` where {@link setName} is the programmatic `'lang-learning'`. One line, at most\n * 100 characters — the Claude Connectors Directory's listing Name limit.\n *\n * The app sets it because the server cannot derive it: it does not read the OpenAPI manifest at\n * runtime, and one MCP server may bind contracts from several api libraries.\n */\n setTitle(title: string): this {\n const value = this.requireText(title, 'setTitle');\n if (/[\\r\\n]/.test(value)) {\n throw new Error('WpMcpServerConfig.setTitle(...) requires a single line of text.');\n }\n this.titleValue = this.requireMaxLength(value, MAX_MCP_SERVER_TITLE_LENGTH, 'setTitle');\n return this;\n }\n\n /**\n * REQUIRED. What this server does, sent as `serverInfo.description` (MCP 2025-11-25). At most\n * 2000 characters — the Claude Connectors Directory's listing Description limit.\n */\n setDescription(description: string): this {\n const value = this.requireText(description, 'setDescription');\n this.descriptionValue = this.requireMaxLength(\n value,\n MAX_MCP_SERVER_DESCRIPTION_LENGTH,\n 'setDescription',\n );\n return this;\n }\n\n /**\n * REQUIRED. An absolute `https://` URL of a page a HUMAN reads about this server — the product\n * homepage or its docs site — sent as `serverInfo.websiteUrl` (MCP 2025-11-25).\n *\n * Deliberately NOT derived from {@link setResource}: that is the MCP endpoint, an API host, not a\n * website, and the resource is never derived from request input either. No default, because the\n * page a user is sent to is the app's decision.\n */\n setWebsiteUrl(websiteUrl: string): this {\n const value = this.requireUrl(websiteUrl, 'setWebsiteUrl');\n if (new URL(value).protocol !== 'https:') {\n throw new Error(\n `WpMcpServerConfig.setWebsiteUrl(...) requires an https:// URL, got '${value}'.`,\n );\n }\n this.websiteUrlValue = value;\n return this;\n }\n\n /** REQUIRED. The MCP server version; the registry revision is appended to it at bind time. */\n setVersion(version: string): this {\n this.versionValue = this.requireText(version, 'setVersion');\n return this;\n }\n\n /** OPTIONAL. Standard MCP icons; copied and frozen, with no network access. */\n setIcons(icons: readonly Icon[]): this {\n this.iconsValue = this.snapshotIcons(icons);\n return this;\n }\n\n /** Immutable snapshot; omitted icons remain absent from SDK implementation metadata. */\n get icons(): readonly McpIcon[] | undefined {\n return this.iconsValue;\n }\n\n /**\n * REQUIRED. The ONE canonical resource URI, which must be an absolute URL: it is the exact\n * audience every `mcpOAuth(...)` access token is checked against, and {@link resourceMetadataUrl}\n * is derived from it for the 401 challenge, so a non-URL here is always a mistake — including a\n * name or version landed on it.\n *\n * It is fixed for the lifetime of the process and must NEVER be derived from the request `Host`\n * header: the boundary compares a token's audience against this value, so a host-derived\n * resource compares two values both taken from caller-controlled input — the confused-deputy\n * shape this check exists to prevent. Its path must equal `McpBindOptions.endpointPath`, which\n * `WpMcpServer.bind(...)` verifies. A second hostname is a second deployment, not a second\n * audience.\n */\n setResource(resource: string): this {\n this.resourceValue = this.requireUrl(resource, 'setResource');\n return this;\n }\n\n /**\n * REQUIRED. HOW a caller gets in: an OR-set of the `@WpAuth` builders endpoints already use —\n * `oidcSameProject()`, `oidcSvcs([...])`, `jwt()`, `apiKey(regime, creds)`, `sharedSecret(key)` —\n * plus `mcpOAuth(...)` for off-the-shelf connectors. Each request is CLASSIFIED from unverified\n * surface facts (which header; a JWT's unverified `iss`) and handed to exactly ONE verifier, whose\n * rejection is the answer — it never falls through to another mode.\n *\n * `vendorAuth(...)` and `@WpAuthPublic` identify no caller to authorize tools against, so they are\n * refused at bind; so is a set whose modes could both claim one request.\n */\n setAuth(auth: McpHttpAuths): this {\n if (!Array.isArray(auth) || auth.length === 0) {\n throw new Error('WpMcpServerConfig.setAuth(...) requires at least one auth mode.');\n }\n this.authValue = Object.freeze([...auth]) as McpHttpAuths;\n return this;\n }\n\n /**\n * REQUIRED when {@link setAuth} holds any `@WpAuth` mode (everything but `mcpOAuth(...)`): the\n * verifiers this app bound, from `router.credentialVerifiers()` — the SAME JwtHook, OidcHook /\n * DefaultOidcVerifier, AuthConfig and ApiKeyHook the endpoint `AuthFilter` uses.\n */\n setCredentialVerifiers(verifiers: CredentialVerifiers): this {\n if (!verifiers || typeof verifiers.verifyOidc !== 'function') {\n throw new Error(\n 'WpMcpServerConfig.setCredentialVerifiers(...) requires router.credentialVerifiers().',\n );\n }\n this.credentialVerifiersValue = verifiers;\n return this;\n }\n\n /**\n * Called by `WpMcpServer.bind(...)`. Throws naming EVERY missing required setter at once, so a\n * half-built config is repaired in one pass rather than one boot failure at a time.\n */\n validate(): void {\n const missing: string[] = [];\n if (this.nameValue === undefined) missing.push('setName(...)');\n if (this.titleValue === undefined) missing.push('setTitle(...)');\n if (this.descriptionValue === undefined) missing.push('setDescription(...)');\n if (this.websiteUrlValue === undefined) missing.push('setWebsiteUrl(...)');\n if (this.versionValue === undefined) missing.push('setVersion(...)');\n if (this.resourceValue === undefined) missing.push('setResource(...)');\n if (this.authValue === undefined) missing.push('setAuth([...])');\n const needsVerifiers = (this.authValue ?? []).some(\n (mode: McpHttpAuth) => mode.kind !== 'mcp-oauth',\n );\n if (needsVerifiers && this.credentialVerifiersValue === undefined) {\n missing.push('setCredentialVerifiers(router.credentialVerifiers())');\n }\n if (missing.length > 0) {\n throw new Error(`WpMcpServerConfig is missing ${missing.join(', ')}`);\n }\n for (const icon of this.iconsValue ?? []) {\n const url = new URL(icon.src);\n if (url.protocol === 'https:' && url.origin !== new URL(this.resource).origin) {\n throw new Error(\n 'WpMcpServerConfig.setIcons(...) requires HTTPS icons on the resource origin.',\n );\n }\n }\n }\n\n get name(): string {\n return this.read(this.nameValue, 'setName');\n }\n\n get title(): string {\n return this.read(this.titleValue, 'setTitle');\n }\n\n get description(): string {\n return this.read(this.descriptionValue, 'setDescription');\n }\n\n get websiteUrl(): string {\n return this.read(this.websiteUrlValue, 'setWebsiteUrl');\n }\n\n get version(): string {\n return this.read(this.versionValue, 'setVersion');\n }\n\n get resource(): string {\n return this.read(this.resourceValue, 'setResource');\n }\n\n get auth(): McpHttpAuths {\n return this.read(this.authValue, 'setAuth');\n }\n\n /** The bound verifiers, or `undefined` for an `mcpOAuth(...)`-only server, which needs none. */\n get credentialVerifiers(): CredentialVerifiers | undefined {\n return this.credentialVerifiersValue;\n }\n\n /**\n * RFC 9728 §3.1: the URL of this resource's protected-resource METADATA DOCUMENT, derived from\n * {@link resource} by inserting `/.well-known/oauth-protected-resource` between origin and path\n * (`https://host/mcp` → `https://host/.well-known/oauth-protected-resource/mcp`).\n *\n * This — never the resource identifier itself — is what RFC 9728 §5.1 defines `resource_metadata`\n * to be, so it is the value webpieces puts in the `mcpOAuth(...)` 401 `WWW-Authenticate`\n * challenge. webpieces mounts no `.well-known` route; publish\n * `WpMcpServer.protectedResourceMetadata()` at this URL.\n */\n get resourceMetadataUrl(): string {\n const url = new URL(this.resource);\n const path = url.pathname === '/' ? '' : url.pathname;\n return `${url.origin}/.well-known/oauth-protected-resource${path}`;\n }\n\n /** Copies caller-owned descriptors and nested size arrays before freezing the snapshot. */\n private snapshotIcons(icons: readonly Icon[]): readonly McpIcon[] {\n if (!Array.isArray(icons)) {\n throw new Error(\n 'WpMcpServerConfig.setIcons(...) requires an array of icon descriptors.',\n );\n }\n return Object.freeze(\n icons.map((icon: Icon) => {\n if (!icon || typeof icon !== 'object') {\n throw new Error('WpMcpServerConfig.setIcons(...) requires icon descriptors.');\n }\n return new McpIcon(icon.src, icon.mimeType, icon.sizes, icon.theme);\n }),\n );\n }\n\n private requireText(value: string, setter: string): string {\n if (typeof value !== 'string' || value.trim() === '') {\n throw new Error(`WpMcpServerConfig.${setter}(...) requires a non-empty string.`);\n }\n return value;\n }\n\n private requireUrl(value: string, setter: string): string {\n this.requireText(value, setter);\n if (!URL.canParse(value)) {\n throw new Error(\n `WpMcpServerConfig.${setter}(...) requires an absolute URL, got '${value}'.`,\n );\n }\n return value;\n }\n\n private requireMaxLength(value: string, max: number, setter: string): string {\n if (value.length > max) {\n throw new Error(\n `WpMcpServerConfig.${setter}(...) allows at most ${max} characters, got ${value.length}.`,\n );\n }\n return value;\n }\n\n private read<T>(value: T | undefined, setter: string): T {\n if (value === undefined) {\n throw new Error(`WpMcpServerConfig is missing ${setter}(...)`);\n }\n return value;\n }\n}\n\n/** Google's OIDC issuers. An `mcpOAuth(...)` issuer may never be one: it would claim Google tokens. */\nexport const GOOGLE_OIDC_ISSUERS: readonly string[] = Object.freeze([\n 'https://accounts.google.com',\n 'accounts.google.com',\n]);\n\n/** The unsigned off-GCP OIDC token prefix `@webpieces/gcp-identity` mints and verifies. */\nexport const DEV_OIDC_TOKEN_PREFIX = 'dev-oidc.';\n\n/** Every JWT starts with the base64url of `{\"`. An opaque prefix starting so would claim JWTs. */\nexport const JWT_PREFIX = 'eyJ';\n\nexport type McpAccessTokenFormatKind = 'opaque-prefix' | 'jwt-issuer' | 'every-bearer';\n\n/** The trusted token issuers of an `mcpOAuth(...)` mode: at least one absolute URL. */\nexport type McpAuthorizationServers = readonly [string, ...string[]];\n\n/**\n * How an `mcpOAuth(...)` access token LOOKS, decided by the authority that mints it. The MCP door\n * classifies every request from unverified surface facts before handing it to exactly ONE verifier, so\n * the OAuth mode must say which bearers are its kind without any crypto or I/O:\n *\n * - {@link opaquePrefix} — the authority mints opaque tokens that all start with `prefix`\n * (e.g. `'wpmcp_'`). Disjoint from every JWT and from off-GCP OIDC tokens by construction.\n * - {@link jwtIssuer} — the authority mints JWTs whose `iss` is one of the mode's\n * `authorizationServers`. A JWT with any other issuer is not this mode's.\n * - {@link everyBearer} — every `Authorization: Bearer` credential is this mode's. Legal ONLY when no\n * other bearer mode (`oidcSameProject()`, `oidcSvcs(...)`, `jwt()`, a bearer `apiKey(...)`) is in the\n * same `setAuth([...])`; startup refuses the combination, because two modes claiming one token is\n * exactly the overlap classify-then-verify forbids.\n */\nexport class McpAccessTokenFormat {\n private constructor(\n public readonly kind: McpAccessTokenFormatKind,\n public readonly prefix: string,\n ) {}\n\n // webpieces-disable no-function-outside-class -- explicit public factory of a closed set of formats\n static opaquePrefix(prefix: string): McpAccessTokenFormat {\n if (typeof prefix !== 'string' || prefix.trim() === '' || /\\s/.test(prefix)) {\n throw new Error(\n 'McpAccessTokenFormat.opaquePrefix(...) requires a nonblank prefix with no whitespace.',\n );\n }\n for (const reserved of [JWT_PREFIX, DEV_OIDC_TOKEN_PREFIX]) {\n if (reserved.startsWith(prefix) || prefix.startsWith(reserved)) {\n throw new Error(\n `McpAccessTokenFormat.opaquePrefix('${prefix}') overlaps '${reserved}', which ` +\n (reserved === JWT_PREFIX\n ? 'every JWT starts with'\n : 'every off-GCP OIDC token starts with') +\n \"; pick a prefix the authority alone mints, e.g. 'wpmcp_'.\",\n );\n }\n }\n return new McpAccessTokenFormat('opaque-prefix', prefix);\n }\n\n // webpieces-disable no-function-outside-class -- explicit public factory of a closed set of formats\n static jwtIssuer(): McpAccessTokenFormat {\n return new McpAccessTokenFormat('jwt-issuer', '');\n }\n\n // webpieces-disable no-function-outside-class -- explicit public factory of a closed set of formats\n static everyBearer(): McpAccessTokenFormat {\n return new McpAccessTokenFormat('every-bearer', '');\n }\n}\n\n/**\n * The MCP OAuth mode: today's connector behavior (Claude / ChatGPT) as ONE member of a server's\n * `setAuth([...])` set. It is the only mode that publishes RFC 9728 protected-resource metadata and\n * answers a request carrying no credential with the `401 WWW-Authenticate` discovery challenge, and\n * its access tokens are audience-bound to the server's canonical resource. Build it with\n * {@link mcpOAuth}.\n */\nexport class McpOAuthMode {\n readonly kind = 'mcp-oauth';\n\n constructor(\n public readonly authority: McpAccessTokenAuthority<never>,\n public readonly authorizationServers: McpAuthorizationServers,\n public readonly requiredScopes: readonly string[],\n public readonly tokenFormat: McpAccessTokenFormat,\n public readonly maxAccountValidationAgeSeconds: number,\n ) {\n if (!authority || typeof authority.verifyAccessToken !== 'function') {\n throw new Error('mcpOAuth(...) requires an McpAccessTokenAuthority.');\n }\n if (!Array.isArray(authorizationServers) || authorizationServers.length === 0) {\n throw new Error('mcpOAuth(...) requires at least one authorization server issuer URL.');\n }\n for (const server of authorizationServers) {\n if (typeof server !== 'string' || !URL.canParse(server)) {\n throw new Error(\n `mcpOAuth(...) authorization servers must be absolute URLs, got '${String(server)}'.`,\n );\n }\n }\n if (\n !Array.isArray(requiredScopes) ||\n !requiredScopes.every(\n (scope: string) => typeof scope === 'string' && scope.trim() !== '',\n )\n ) {\n throw new Error(\n 'mcpOAuth(...) requires an array of nonblank required scopes (empty only by saying so: []).',\n );\n }\n if (!(tokenFormat instanceof McpAccessTokenFormat)) {\n throw new Error(\n 'mcpOAuth(...) requires an McpAccessTokenFormat describing the tokens the authority mints.',\n );\n }\n if (\n !Number.isFinite(maxAccountValidationAgeSeconds) ||\n maxAccountValidationAgeSeconds <= 0 ||\n maxAccountValidationAgeSeconds > MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS\n ) {\n throw new Error(\n `mcpOAuth(...) maxAccountValidationAgeSeconds must be 1..${MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS}, ` +\n `got ${String(maxAccountValidationAgeSeconds)}.`,\n );\n }\n Object.freeze(this);\n }\n\n /** RFC 9728 metadata for `resource`, published by the app at the server's `resourceMetadataUrl`. */\n protectedResourceMetadata(resource: string): McpProtectedResourceMetadata {\n return new McpProtectedResourceMetadata(\n resource,\n this.authorizationServers,\n this.requiredScopes,\n );\n }\n}\n\n/**\n * The MCP OAuth door mode, for off-the-shelf connectors. Everything OAuth-only lives here and is\n * required only when this mode is in the server's `setAuth([...])`:\n *\n * @param authority the application-owned paired mint/verify authority\n * @param authorizationServers the trusted token issuers, published as `authorization_servers`\n * @param requiredScopes scopes every access token must carry before any tool is dispatched\n * @param tokenFormat how the authority's tokens look, so the door can classify them\n * @param maxAccountValidationAgeSeconds how fresh the account facts must be, 1 second to 1 hour\n */\n// webpieces-disable no-function-outside-class -- canonical credential descriptor factory, the twin of jwt()/oidcSameProject()\nexport function mcpOAuth<TGrant>(\n authority: McpAccessTokenAuthority<TGrant>,\n authorizationServers: McpAuthorizationServers,\n requiredScopes: readonly string[],\n tokenFormat: McpAccessTokenFormat,\n maxAccountValidationAgeSeconds: number = MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS,\n): McpOAuthMode {\n return new McpOAuthMode(\n authority as McpAccessTokenAuthority<never>,\n authorizationServers,\n requiredScopes,\n tokenFormat,\n maxAccountValidationAgeSeconds,\n );\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { AuthenticatedCaller, JwtHook, MintedJwt } from '@webpieces/http-routing';
|
|
1
|
+
import { AuthenticatedCaller, AuthorizationService, JwtHook, MintedJwt } from '@webpieces/http-routing';
|
|
2
2
|
import { McpAccessTokenAuthority, MintedMcpAccessToken, VerifiedMcpCredential, WpMcpServerConfig } from './McpAuth';
|
|
3
|
+
import { McpApiBinding } from './McpApiBinding';
|
|
3
4
|
declare class ExampleJwtMintRequest {
|
|
4
5
|
readonly subject: string;
|
|
5
6
|
constructor(subject: string);
|
|
@@ -22,6 +23,15 @@ export declare class McpPrincipalCompileAssertions {
|
|
|
22
23
|
}
|
|
23
24
|
/** Profile-selection arity is checked by tsc, not only by runtime specs. */
|
|
24
25
|
export declare class McpProfilesCompileAssertions {
|
|
25
|
-
empty(
|
|
26
|
+
empty(bindings: readonly McpApiBinding[], policy: AuthorizationService): void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The OAuth-only setters moved into `mcpOAuth(...)` (#1416) and the toolset owns WHICH tools: none of
|
|
30
|
+
* the old spellings compiles, so an agent upgrading is told by tsc, not by a deprecation it never sees.
|
|
31
|
+
*/
|
|
32
|
+
export declare class McpServerConfigCompileAssertions {
|
|
33
|
+
removedOAuthSetters(config: WpMcpServerConfig, authority: McpAccessTokenAuthority<string>): void;
|
|
34
|
+
noPublicDoor(config: WpMcpServerConfig): void;
|
|
35
|
+
removedToolsetSetters(config: WpMcpServerConfig, policy: AuthorizationService): void;
|
|
26
36
|
}
|
|
27
37
|
export {};
|