@webpieces/mcp-server 0.4.766 → 0.4.768

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,9 +43,17 @@ response classes generate `inputSchema` and `outputSchema`; `@WpDtoField` suppli
43
43
  descriptions and the facts TypeScript erases, such as optionality and array element types.
44
44
 
45
45
  Applications construct `WpMcpServer` with their built `ApiFactory`, the API classes they want scanned,
46
- and an `McpAccessTokenVerifier`. The verifier checks issuer, signature, expiry, scopes, and the exact
47
- resource URI, then returns an endpoint bearer credential. The bridge uses that credential only for an
48
- in-process call through the ordinary `AuthFilter`; it never forwards the MCP token to downstream APIs.
46
+ a paired `McpAccessTokenAuthority`, and their application `JwtHook`. The authority verifies issuer,
47
+ signature/token state, expiry, scopes, the exact resource URI, and current account state on every MCP
48
+ operation. Before every tool call the bridge asks the same application `JwtHook` to mint a distinct,
49
+ short-lived endpoint JWT, then invokes the ordinary `AuthFilter`. Literal MCP-token passthrough is
50
+ rejected, endpoint JWTs are capped at one hour, and MCP access tokens are capped at 30 days.
51
+
52
+ The verifier's `accountValidatedAtEpochSeconds` must represent an authoritative enabled/revoked and
53
+ role/scope read. The bridge enforces a maximum one-hour decision age; per-request reads are preferred so
54
+ offboarding and role changes take effect on the next call. JWT access-token implementations must enforce
55
+ an explicit algorithm allowlist plus issuer, exact audience/resource, expiry, type/version, and key
56
+ rotation metadata. Opaque tokens remain valid implementations of the same authority contract.
49
57
 
50
58
  `protectedResourceMetadata()` returns the resource metadata an HTTP adapter can publish at the
51
59
  well-known OAuth protected-resource endpoint. OAuth token issuance remains pluggable.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/mcp-server",
3
- "version": "0.4.766",
3
+ "version": "0.4.768",
4
4
  "description": "Secure MCP tool generation from annotated Webpieces API contracts",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -23,9 +23,9 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@modelcontextprotocol/sdk": "1.29.0",
26
- "@webpieces/core-context": "0.4.766",
27
- "@webpieces/core-util": "0.4.766",
28
- "@webpieces/http-routing": "0.4.766",
26
+ "@webpieces/core-context": "0.4.768",
27
+ "@webpieces/core-util": "0.4.768",
28
+ "@webpieces/http-routing": "0.4.768",
29
29
  "reflect-metadata": "0.2.2",
30
30
  "tslib": "2.8.1"
31
31
  }
package/src/McpAuth.d.ts CHANGED
@@ -1,26 +1,51 @@
1
- /** Credential accepted at the MCP resource boundary and the local endpoint credential it resolves to. */
1
+ import { JwtHook } from '@webpieces/http-routing';
2
+ export declare const MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS: number;
3
+ export declare const MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS: number;
4
+ export declare const MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS: number;
5
+ /** Framework-normalized access-token result. The token representation remains application-owned. */
6
+ export declare class MintedMcpAccessToken {
7
+ readonly token: string;
8
+ readonly resource: string;
9
+ readonly issuedAtEpochSeconds: number;
10
+ readonly expiresAtEpochSeconds: number;
11
+ constructor(token: string, resource: string, issuedAtEpochSeconds: number, expiresAtEpochSeconds: number);
12
+ }
13
+ /**
14
+ * Fresh, authoritative security facts resolved at the MCP resource boundary. Implementations must
15
+ * verify issuer, exact resource/audience, expiry, token type/version, key/algorithm policy, and
16
+ * current account state. Opaque access tokens are fully supported.
17
+ */
2
18
  export declare class VerifiedMcpCredential {
3
- /** Used only on the in-process endpoint invocation; never forwarded to another service. */
4
- readonly endpointBearerToken: string;
19
+ readonly subject: string;
5
20
  readonly issuer: string;
6
- /** The RFC 8707 resource/audience the verifier proved from the access token. */
7
21
  readonly resource: string;
22
+ readonly issuedAtEpochSeconds: number;
8
23
  readonly expiresAtEpochSeconds: number;
9
24
  readonly scopes: readonly string[];
25
+ /** When current enabled/revoked state and roles were read from the authoritative source. */
26
+ readonly accountValidatedAtEpochSeconds: number;
10
27
  /** Advisory tools/list filtering only. Endpoint authorization always runs again. */
11
28
  readonly listingRoles: readonly string[];
12
- constructor(
13
- /** Used only on the in-process endpoint invocation; never forwarded to another service. */
14
- endpointBearerToken: string, issuer: string,
15
- /** The RFC 8707 resource/audience the verifier proved from the access token. */
16
- resource: string, expiresAtEpochSeconds: number, scopes: readonly string[],
29
+ constructor(subject: string, issuer: string, resource: string, issuedAtEpochSeconds: number, expiresAtEpochSeconds: number, scopes: readonly string[],
30
+ /** When current enabled/revoked state and roles were read from the authoritative source. */
31
+ accountValidatedAtEpochSeconds: number,
17
32
  /** Advisory tools/list filtering only. Endpoint authorization always runs again. */
18
33
  listingRoles?: readonly string[]);
19
34
  }
20
- /** Application-owned verification/token-exchange seam for a resource-bound MCP access token. */
21
- export declare abstract class McpAccessTokenVerifier {
22
- abstract verify(accessToken: string, expectedResource: string): Promise<VerifiedMcpCredential>;
35
+ /** Application-owned paired mint/verify authority for resource-bound MCP access tokens. */
36
+ export interface McpAccessTokenAuthority<TGrant> {
37
+ mintAccessToken(grant: TGrant): Promise<MintedMcpAccessToken>;
38
+ verifyAccessToken(accessToken: string, expectedResource: string): Promise<VerifiedMcpCredential>;
39
+ }
40
+ /** Minimal tool identity supplied when the app creates its own endpoint-JWT mint request. */
41
+ export declare class McpEndpointDescriptor {
42
+ readonly toolName: string;
43
+ readonly apiClassName: string;
44
+ readonly methodName: string;
45
+ constructor(toolName: string, apiClassName: string, methodName: string);
23
46
  }
47
+ /** Application mapping from verified MCP identity to its unconstrained JwtHook mint request. */
48
+ export type McpEndpointMintRequestFactory<TMintRequest> = (credential: VerifiedMcpCredential, endpoint: McpEndpointDescriptor) => TMintRequest;
24
49
  /** RFC 9728-style metadata exposed by the MCP protected resource. */
25
50
  export declare class McpProtectedResourceMetadata {
26
51
  readonly resource: string;
@@ -29,14 +54,21 @@ export declare class McpProtectedResourceMetadata {
29
54
  readonly scopes_supported?: readonly string[];
30
55
  constructor(resource: string, authorizationServers: readonly string[], scopesSupported?: readonly string[]);
31
56
  }
32
- /** Node MCP server configuration. OAuth token issuance stays with the application/identity provider. */
33
- export declare class WpMcpServerConfig {
57
+ /**
58
+ * Node MCP server configuration. Pass the same concrete application authority as both
59
+ * accessTokenAuthority and endpointJwtAuthority when it implements both security seams.
60
+ */
61
+ export declare class WpMcpServerConfig<TGrant, TMintRequest> {
34
62
  readonly name: string;
35
63
  readonly version: string;
36
64
  readonly resource: string;
37
- readonly tokenVerifier: McpAccessTokenVerifier;
65
+ readonly accessTokenAuthority: McpAccessTokenAuthority<TGrant>;
66
+ readonly endpointJwtAuthority: JwtHook<TMintRequest>;
67
+ readonly endpointMintRequest: McpEndpointMintRequestFactory<TMintRequest>;
38
68
  readonly authorizationServers: readonly string[];
39
69
  readonly requiredScopes: readonly string[];
40
- constructor(name: string, version: string, resource: string, tokenVerifier: McpAccessTokenVerifier, authorizationServers: readonly string[], requiredScopes: readonly string[]);
70
+ readonly maxAccountValidationAgeSeconds: number;
71
+ readonly maxEndpointJwtLifetimeSeconds: number;
72
+ constructor(name: string, version: string, resource: string, accessTokenAuthority: McpAccessTokenAuthority<TGrant>, endpointJwtAuthority: JwtHook<TMintRequest>, endpointMintRequest: McpEndpointMintRequestFactory<TMintRequest>, authorizationServers: readonly string[], requiredScopes: readonly string[], maxAccountValidationAgeSeconds?: number, maxEndpointJwtLifetimeSeconds?: number);
41
73
  protectedResourceMetadata(): McpProtectedResourceMetadata;
42
74
  }
package/src/McpAuth.js CHANGED
@@ -1,34 +1,77 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WpMcpServerConfig = exports.McpProtectedResourceMetadata = exports.McpAccessTokenVerifier = exports.VerifiedMcpCredential = void 0;
4
- /** Credential accepted at the MCP resource boundary and the local endpoint credential it resolves to. */
3
+ exports.WpMcpServerConfig = exports.McpProtectedResourceMetadata = exports.McpEndpointDescriptor = exports.VerifiedMcpCredential = exports.MintedMcpAccessToken = exports.MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS = exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS = exports.MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS = void 0;
4
+ exports.MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
5
+ exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS = 60 * 60;
6
+ exports.MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS = 60 * 60;
7
+ /** Framework-normalized access-token result. The token representation remains application-owned. */
8
+ class MintedMcpAccessToken {
9
+ token;
10
+ resource;
11
+ issuedAtEpochSeconds;
12
+ expiresAtEpochSeconds;
13
+ constructor(token, resource, issuedAtEpochSeconds, expiresAtEpochSeconds) {
14
+ this.token = token;
15
+ this.resource = resource;
16
+ this.issuedAtEpochSeconds = issuedAtEpochSeconds;
17
+ this.expiresAtEpochSeconds = expiresAtEpochSeconds;
18
+ if (token.trim() === '' || resource.trim() === '') {
19
+ throw new Error('Minted MCP access token and resource must be non-empty.');
20
+ }
21
+ if (!Number.isFinite(issuedAtEpochSeconds) || !Number.isFinite(expiresAtEpochSeconds)) {
22
+ throw new Error('Minted MCP access token timestamps must be finite epoch-second values.');
23
+ }
24
+ if (expiresAtEpochSeconds <= issuedAtEpochSeconds) {
25
+ throw new Error('MCP access token expiry must be after issuance.');
26
+ }
27
+ if (expiresAtEpochSeconds - issuedAtEpochSeconds > exports.MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS) {
28
+ throw new Error('MCP access token lifetime must not exceed 30 days.');
29
+ }
30
+ }
31
+ }
32
+ exports.MintedMcpAccessToken = MintedMcpAccessToken;
33
+ /**
34
+ * Fresh, authoritative security facts resolved at the MCP resource boundary. Implementations must
35
+ * verify issuer, exact resource/audience, expiry, token type/version, key/algorithm policy, and
36
+ * current account state. Opaque access tokens are fully supported.
37
+ */
5
38
  class VerifiedMcpCredential {
6
- endpointBearerToken;
39
+ subject;
7
40
  issuer;
8
41
  resource;
42
+ issuedAtEpochSeconds;
9
43
  expiresAtEpochSeconds;
10
44
  scopes;
45
+ accountValidatedAtEpochSeconds;
11
46
  listingRoles;
12
- constructor(
13
- /** Used only on the in-process endpoint invocation; never forwarded to another service. */
14
- endpointBearerToken, issuer,
15
- /** The RFC 8707 resource/audience the verifier proved from the access token. */
16
- resource, expiresAtEpochSeconds, scopes,
47
+ constructor(subject, issuer, resource, issuedAtEpochSeconds, expiresAtEpochSeconds, scopes,
48
+ /** When current enabled/revoked state and roles were read from the authoritative source. */
49
+ accountValidatedAtEpochSeconds,
17
50
  /** Advisory tools/list filtering only. Endpoint authorization always runs again. */
18
51
  listingRoles = []) {
19
- this.endpointBearerToken = endpointBearerToken;
52
+ this.subject = subject;
20
53
  this.issuer = issuer;
21
54
  this.resource = resource;
55
+ this.issuedAtEpochSeconds = issuedAtEpochSeconds;
22
56
  this.expiresAtEpochSeconds = expiresAtEpochSeconds;
23
57
  this.scopes = scopes;
58
+ this.accountValidatedAtEpochSeconds = accountValidatedAtEpochSeconds;
24
59
  this.listingRoles = listingRoles;
25
60
  }
26
61
  }
27
62
  exports.VerifiedMcpCredential = VerifiedMcpCredential;
28
- /** Application-owned verification/token-exchange seam for a resource-bound MCP access token. */
29
- class McpAccessTokenVerifier {
63
+ /** Minimal tool identity supplied when the app creates its own endpoint-JWT mint request. */
64
+ class McpEndpointDescriptor {
65
+ toolName;
66
+ apiClassName;
67
+ methodName;
68
+ constructor(toolName, apiClassName, methodName) {
69
+ this.toolName = toolName;
70
+ this.apiClassName = apiClassName;
71
+ this.methodName = methodName;
72
+ }
30
73
  }
31
- exports.McpAccessTokenVerifier = McpAccessTokenVerifier;
74
+ exports.McpEndpointDescriptor = McpEndpointDescriptor;
32
75
  /** RFC 9728-style metadata exposed by the MCP protected resource. */
33
76
  class McpProtectedResourceMetadata {
34
77
  resource;
@@ -42,24 +85,43 @@ class McpProtectedResourceMetadata {
42
85
  }
43
86
  }
44
87
  exports.McpProtectedResourceMetadata = McpProtectedResourceMetadata;
45
- /** Node MCP server configuration. OAuth token issuance stays with the application/identity provider. */
88
+ /**
89
+ * Node MCP server configuration. Pass the same concrete application authority as both
90
+ * accessTokenAuthority and endpointJwtAuthority when it implements both security seams.
91
+ */
46
92
  class WpMcpServerConfig {
47
93
  name;
48
94
  version;
49
95
  resource;
50
- tokenVerifier;
96
+ accessTokenAuthority;
97
+ endpointJwtAuthority;
98
+ endpointMintRequest;
51
99
  authorizationServers;
52
100
  requiredScopes;
53
- constructor(name, version, resource, tokenVerifier, authorizationServers, requiredScopes) {
101
+ maxAccountValidationAgeSeconds;
102
+ maxEndpointJwtLifetimeSeconds;
103
+ constructor(name, version, resource, accessTokenAuthority, endpointJwtAuthority, endpointMintRequest, authorizationServers, requiredScopes, maxAccountValidationAgeSeconds = exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS, maxEndpointJwtLifetimeSeconds = exports.MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS) {
54
104
  this.name = name;
55
105
  this.version = version;
56
106
  this.resource = resource;
57
- this.tokenVerifier = tokenVerifier;
107
+ this.accessTokenAuthority = accessTokenAuthority;
108
+ this.endpointJwtAuthority = endpointJwtAuthority;
109
+ this.endpointMintRequest = endpointMintRequest;
58
110
  this.authorizationServers = authorizationServers;
59
111
  this.requiredScopes = requiredScopes;
112
+ this.maxAccountValidationAgeSeconds = maxAccountValidationAgeSeconds;
113
+ this.maxEndpointJwtLifetimeSeconds = maxEndpointJwtLifetimeSeconds;
60
114
  if (name.trim() === '' || version.trim() === '' || resource.trim() === '') {
61
115
  throw new Error('MCP name, version, and resource must be non-empty.');
62
116
  }
117
+ if (maxAccountValidationAgeSeconds <= 0 ||
118
+ maxAccountValidationAgeSeconds > exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS) {
119
+ throw new Error('MCP account validation cache ceiling must be between 1 second and 1 hour.');
120
+ }
121
+ if (maxEndpointJwtLifetimeSeconds <= 0 ||
122
+ maxEndpointJwtLifetimeSeconds > exports.MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS) {
123
+ throw new Error('MCP endpoint JWT lifetime must be between 1 second and 1 hour.');
124
+ }
63
125
  }
64
126
  protectedResourceMetadata() {
65
127
  return new McpProtectedResourceMetadata(this.resource, this.authorizationServers, this.requiredScopes);
@@ -1 +1 @@
1
- {"version":3,"file":"McpAuth.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpAuth.ts"],"names":[],"mappings":";;;AAAA,yGAAyG;AACzG,MAAa,qBAAqB;IAGV;IACA;IAEA;IACA;IACA;IAEA;IATpB;IACI,2FAA2F;IAC3E,mBAA2B,EAC3B,MAAc;IAC9B,gFAAgF;IAChE,QAAgB,EAChB,qBAA6B,EAC7B,MAAyB;IACzC,oFAAoF;IACpE,eAAkC,EAAE;QAPpC,wBAAmB,GAAnB,mBAAmB,CAAQ;QAC3B,WAAM,GAAN,MAAM,CAAQ;QAEd,aAAQ,GAAR,QAAQ,CAAQ;QAChB,0BAAqB,GAArB,qBAAqB,CAAQ;QAC7B,WAAM,GAAN,MAAM,CAAmB;QAEzB,iBAAY,GAAZ,YAAY,CAAwB;IACrD,CAAC;CACP;AAZD,sDAYC;AAED,gGAAgG;AAChG,MAAsB,sBAAsB;CAK3C;AALD,wDAKC;AAED,qEAAqE;AACrE,MAAa,4BAA4B;IAC5B,QAAQ,CAAS;IACjB,qBAAqB,CAAoB;IACzC,wBAAwB,GAAG,CAAC,QAAQ,CAAC,CAAC;IACtC,gBAAgB,CAAqB;IAE9C,YACI,QAAgB,EAChB,oBAAuC,EACvC,eAAmC;QAEnC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,qBAAqB,GAAG,oBAAoB,CAAC;QAClD,IAAI,CAAC,gBAAgB,GAAG,eAAe,CAAC;IAC5C,CAAC;CACJ;AAfD,oEAeC;AAED,wGAAwG;AACxG,MAAa,iBAAiB;IAEN;IACA;IACA;IACA;IACA;IACA;IANpB,YACoB,IAAY,EACZ,OAAe,EACf,QAAgB,EAChB,aAAqC,EACrC,oBAAuC,EACvC,cAAiC;QALjC,SAAI,GAAJ,IAAI,CAAQ;QACZ,YAAO,GAAP,OAAO,CAAQ;QACf,aAAQ,GAAR,QAAQ,CAAQ;QAChB,kBAAa,GAAb,aAAa,CAAwB;QACrC,yBAAoB,GAApB,oBAAoB,CAAmB;QACvC,mBAAc,GAAd,cAAc,CAAmB;QAEjD,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACxE,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;QAC1E,CAAC;IACL,CAAC;IAED,yBAAyB;QACrB,OAAO,IAAI,4BAA4B,CACnC,IAAI,CAAC,QAAQ,EACb,IAAI,CAAC,oBAAoB,EACzB,IAAI,CAAC,cAAc,CACtB,CAAC;IACN,CAAC;CACJ;AArBD,8CAqBC","sourcesContent":["/** Credential accepted at the MCP resource boundary and the local endpoint credential it resolves to. */\nexport class VerifiedMcpCredential {\n constructor(\n /** Used only on the in-process endpoint invocation; never forwarded to another service. */\n public readonly endpointBearerToken: string,\n public readonly issuer: string,\n /** The RFC 8707 resource/audience the verifier proved from the access token. */\n public readonly resource: string,\n public readonly expiresAtEpochSeconds: number,\n public readonly scopes: readonly string[],\n /** Advisory tools/list filtering only. Endpoint authorization always runs again. */\n public readonly listingRoles: readonly string[] = [],\n ) {}\n}\n\n/** Application-owned verification/token-exchange seam for a resource-bound MCP access token. */\nexport abstract class McpAccessTokenVerifier {\n abstract verify(\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/** Node MCP server configuration. OAuth token issuance stays with the application/identity provider. */\nexport class WpMcpServerConfig {\n constructor(\n public readonly name: string,\n public readonly version: string,\n public readonly resource: string,\n public readonly tokenVerifier: McpAccessTokenVerifier,\n public readonly authorizationServers: readonly string[],\n public readonly requiredScopes: readonly string[],\n ) {\n if (name.trim() === '' || version.trim() === '' || resource.trim() === '') {\n throw new Error('MCP name, version, and resource must be non-empty.');\n }\n }\n\n protectedResourceMetadata(): McpProtectedResourceMetadata {\n return new McpProtectedResourceMetadata(\n this.resource,\n this.authorizationServers,\n this.requiredScopes,\n );\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 { JwtHook } from '@webpieces/http-routing';\n\nexport const MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS = 30 * 24 * 60 * 60;\nexport const MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS = 60 * 60;\nexport const MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS = 60 * 60;\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('Minted MCP access token timestamps must be finite epoch-second values.');\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 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 /** Advisory tools/list filtering only. Endpoint authorization always runs again. */\n public readonly listingRoles: readonly string[] = [],\n ) {}\n}\n\n/** Application-owned paired mint/verify authority for resource-bound MCP access tokens. */\nexport interface McpAccessTokenAuthority<TGrant> {\n mintAccessToken(grant: TGrant): Promise<MintedMcpAccessToken>;\n verifyAccessToken(\n accessToken: string,\n expectedResource: string,\n ): Promise<VerifiedMcpCredential>;\n}\n\n/** Minimal tool identity supplied when the app creates its own endpoint-JWT mint request. */\nexport class McpEndpointDescriptor {\n constructor(\n public readonly toolName: string,\n public readonly apiClassName: string,\n public readonly methodName: string,\n ) {}\n}\n\n/** Application mapping from verified MCP identity to its unconstrained JwtHook mint request. */\nexport type McpEndpointMintRequestFactory<TMintRequest> = (\n credential: VerifiedMcpCredential,\n endpoint: McpEndpointDescriptor,\n) => TMintRequest;\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. Pass the same concrete application authority as both\n * accessTokenAuthority and endpointJwtAuthority when it implements both security seams.\n */\nexport class WpMcpServerConfig<TGrant, TMintRequest> {\n constructor(\n public readonly name: string,\n public readonly version: string,\n public readonly resource: string,\n public readonly accessTokenAuthority: McpAccessTokenAuthority<TGrant>,\n public readonly endpointJwtAuthority: JwtHook<TMintRequest>,\n public readonly endpointMintRequest: McpEndpointMintRequestFactory<TMintRequest>,\n public readonly authorizationServers: readonly string[],\n public readonly requiredScopes: readonly string[],\n public readonly maxAccountValidationAgeSeconds = MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS,\n public readonly maxEndpointJwtLifetimeSeconds = MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS,\n ) {\n if (name.trim() === '' || version.trim() === '' || resource.trim() === '') {\n throw new Error('MCP name, version, and resource must be non-empty.');\n }\n if (\n maxAccountValidationAgeSeconds <= 0 ||\n maxAccountValidationAgeSeconds > MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS\n ) {\n throw new Error('MCP account validation cache ceiling must be between 1 second and 1 hour.');\n }\n if (\n maxEndpointJwtLifetimeSeconds <= 0 ||\n maxEndpointJwtLifetimeSeconds > MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS\n ) {\n throw new Error('MCP endpoint JWT lifetime must be between 1 second and 1 hour.');\n }\n }\n\n protectedResourceMetadata(): McpProtectedResourceMetadata {\n return new McpProtectedResourceMetadata(\n this.resource,\n this.authorizationServers,\n this.requiredScopes,\n );\n }\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import { AuthenticatedCaller, JwtHook, MintedJwt } from '@webpieces/http-routing';
2
+ import { McpAccessTokenAuthority, MintedMcpAccessToken, VerifiedMcpCredential } from './McpAuth';
3
+ declare class ExampleJwtMintRequest {
4
+ readonly subject: string;
5
+ constructor(subject: string);
6
+ }
7
+ declare class ExampleMcpGrant {
8
+ readonly subject: string;
9
+ readonly resource: string;
10
+ constructor(subject: string, resource: string);
11
+ }
12
+ /** Compile assertion: one application authority can own both endpoint JWT and MCP token lifecycles. */
13
+ export declare class McpAuthorityCompileAssertions extends JwtHook<ExampleJwtMintRequest> implements McpAccessTokenAuthority<ExampleMcpGrant> {
14
+ mint(request: ExampleJwtMintRequest): Promise<MintedJwt>;
15
+ parseJwt(token: string): Promise<AuthenticatedCaller>;
16
+ mintAccessToken(grant: ExampleMcpGrant): Promise<MintedMcpAccessToken>;
17
+ verifyAccessToken(_accessToken: string, expectedResource: string): Promise<VerifiedMcpCredential>;
18
+ }
19
+ export {};
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.McpAuthorityCompileAssertions = void 0;
4
+ const http_routing_1 = require("@webpieces/http-routing");
5
+ const McpAuth_1 = require("./McpAuth");
6
+ class ExampleJwtMintRequest {
7
+ subject;
8
+ constructor(subject) {
9
+ this.subject = subject;
10
+ }
11
+ }
12
+ class ExampleMcpGrant {
13
+ subject;
14
+ resource;
15
+ constructor(subject, resource) {
16
+ this.subject = subject;
17
+ this.resource = resource;
18
+ }
19
+ }
20
+ /** Compile assertion: one application authority can own both endpoint JWT and MCP token lifecycles. */
21
+ class McpAuthorityCompileAssertions extends http_routing_1.JwtHook {
22
+ async mint(request) {
23
+ return new http_routing_1.MintedJwt(`endpoint:${request.subject}`, Date.now() / 1000 + 60);
24
+ }
25
+ async parseJwt(token) {
26
+ return new http_routing_1.AuthenticatedCaller(token);
27
+ }
28
+ async mintAccessToken(grant) {
29
+ const now = Math.floor(Date.now() / 1000);
30
+ return new McpAuth_1.MintedMcpAccessToken(`mcp:${grant.subject}`, grant.resource, now, now + 60);
31
+ }
32
+ async verifyAccessToken(_accessToken, expectedResource) {
33
+ const now = Math.floor(Date.now() / 1000);
34
+ return new McpAuth_1.VerifiedMcpCredential('example-user', 'https://issuer.example.test', expectedResource, now, now + 60, ['tools'], now);
35
+ }
36
+ }
37
+ exports.McpAuthorityCompileAssertions = McpAuthorityCompileAssertions;
38
+ //# sourceMappingURL=McpAuthorityCompileAssertions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"McpAuthorityCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpAuthorityCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,0DAIiC;AACjC,uCAImB;AAEnB,MAAM,qBAAqB;IACK;IAA5B,YAA4B,OAAe;QAAf,YAAO,GAAP,OAAO,CAAQ;IAAG,CAAC;CAClD;AAED,MAAM,eAAe;IACW;IAAiC;IAA7D,YAA4B,OAAe,EAAkB,QAAgB;QAAjD,YAAO,GAAP,OAAO,CAAQ;QAAkB,aAAQ,GAAR,QAAQ,CAAQ;IAAG,CAAC;CACpF;AAED,uGAAuG;AACvG,MAAa,6BAA8B,SAAQ,sBAA8B;IAGpE,KAAK,CAAC,IAAI,CAAC,OAA8B;QAC9C,OAAO,IAAI,wBAAS,CAAC,YAAY,OAAO,CAAC,OAAO,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC;IAChF,CAAC;IAEQ,KAAK,CAAC,QAAQ,CAAC,KAAa;QACjC,OAAO,IAAI,kCAAmB,CAAC,KAAK,CAAC,CAAC;IAC1C,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,KAAsB;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC1C,OAAO,IAAI,8BAAoB,CAAC,OAAO,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC;IAC3F,CAAC;IAED,KAAK,CAAC,iBAAiB,CACnB,YAAoB,EACpB,gBAAwB;QAExB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC1C,OAAO,IAAI,+BAAqB,CAC5B,cAAc,EACd,6BAA6B,EAC7B,gBAAgB,EAChB,GAAG,EACH,GAAG,GAAG,EAAE,EACR,CAAC,OAAO,CAAC,EACT,GAAG,CACN,CAAC;IACN,CAAC;CACJ;AA/BD,sEA+BC","sourcesContent":["import {\n AuthenticatedCaller,\n JwtHook,\n MintedJwt,\n} from '@webpieces/http-routing';\nimport {\n McpAccessTokenAuthority,\n MintedMcpAccessToken,\n VerifiedMcpCredential,\n} from './McpAuth';\n\nclass ExampleJwtMintRequest {\n constructor(public readonly subject: string) {}\n}\n\nclass ExampleMcpGrant {\n constructor(public readonly subject: string, public readonly resource: string) {}\n}\n\n/** Compile assertion: one application authority can own both endpoint JWT and MCP token lifecycles. */\nexport class McpAuthorityCompileAssertions extends JwtHook<ExampleJwtMintRequest>\n implements McpAccessTokenAuthority<ExampleMcpGrant>\n{\n override async mint(request: ExampleJwtMintRequest): Promise<MintedJwt> {\n return new MintedJwt(`endpoint:${request.subject}`, Date.now() / 1000 + 60);\n }\n\n override async parseJwt(token: string): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller(token);\n }\n\n async mintAccessToken(grant: ExampleMcpGrant): Promise<MintedMcpAccessToken> {\n const now = Math.floor(Date.now() / 1000);\n return new MintedMcpAccessToken(`mcp:${grant.subject}`, grant.resource, now, now + 60);\n }\n\n async verifyAccessToken(\n _accessToken: string,\n expectedResource: string,\n ): Promise<VerifiedMcpCredential> {\n const now = Math.floor(Date.now() / 1000);\n return new VerifiedMcpCredential(\n 'example-user',\n 'https://issuer.example.test',\n expectedResource,\n now,\n now + 60,\n ['tools'],\n now,\n );\n }\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { ApiFactory, ClassType } from '@webpieces/http-routing';
3
- import { WpMcpServerConfig } from './McpAuth';
3
+ import { McpProtectedResourceMetadata, WpMcpServerConfig } from './McpAuth';
4
4
  /** The only error shape rendered into model-visible MCP content. */
5
5
  export declare class ModelVisibleToolError {
6
6
  readonly kind: string;
@@ -16,16 +16,18 @@ export declare class ModelVisibleToolError {
16
16
  * Node-only MCP adapter. Build one Server per authenticated transport/session; every tool call still
17
17
  * re-enters the endpoint's ordinary AuthFilter, so tools/list visibility never grants access.
18
18
  */
19
- export declare class WpMcpServer {
19
+ export declare class WpMcpServer<TGrant, TMintRequest> {
20
20
  private readonly config;
21
21
  private readonly registry;
22
22
  private readonly dispatcher;
23
- constructor(config: WpMcpServerConfig, apiFactory: ApiFactory, apiClasses: readonly ClassType[]);
23
+ constructor(config: WpMcpServerConfig<TGrant, TMintRequest>, apiFactory: ApiFactory, apiClasses: readonly ClassType[]);
24
24
  build(accessToken: string): Promise<Server>;
25
- protectedResourceMetadata(): ReturnType<WpMcpServerConfig['protectedResourceMetadata']>;
26
- private buildForCredential;
25
+ protectedResourceMetadata(): McpProtectedResourceMetadata;
26
+ private buildForAccessToken;
27
+ private verifyCredential;
27
28
  private validateCredential;
28
29
  private call;
30
+ private validateEndpointJwt;
29
31
  private apiError;
30
32
  private errorResult;
31
33
  private record;
@@ -4,6 +4,7 @@ exports.WpMcpServer = exports.ModelVisibleToolError = void 0;
4
4
  const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
5
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
6
6
  const core_util_1 = require("@webpieces/core-util");
7
+ const McpAuth_1 = require("./McpAuth");
7
8
  const McpApiDispatcher_1 = require("./McpApiDispatcher");
8
9
  const McpToolRegistry_1 = require("./McpToolRegistry");
9
10
  const log = core_util_1.LogManager.getLogger('WpMcpServer');
@@ -41,60 +42,116 @@ class WpMcpServer {
41
42
  this.dispatcher = new McpApiDispatcher_1.McpApiDispatcher(apiFactory);
42
43
  }
43
44
  async build(accessToken) {
44
- const credential = await this.config.tokenVerifier.verify(accessToken, this.config.resource);
45
- this.validateCredential(credential);
46
- return this.buildForCredential(credential);
45
+ await this.verifyCredential(accessToken);
46
+ return this.buildForAccessToken(accessToken);
47
47
  }
48
48
  protectedResourceMetadata() {
49
49
  return this.config.protectedResourceMetadata();
50
50
  }
51
- buildForCredential(credential) {
51
+ buildForAccessToken(accessToken) {
52
52
  const server = new index_js_1.Server(
53
53
  // webpieces-disable no-anonymous-object-literals -- external MCP SDK request structure
54
54
  { name: this.config.name, version: this.config.version },
55
55
  // webpieces-disable no-anonymous-object-literals -- external MCP SDK capability structure
56
56
  { capabilities: { tools: {} } });
57
- server.setRequestHandler(types_js_1.ListToolsRequestSchema, () => ({
58
- tools: this.registry.tools
59
- .filter((tool) => tool.isVisibleTo(credential.listingRoles))
60
- .map((tool) => ({
61
- name: tool.name,
62
- description: tool.description,
63
- inputSchema: tool.inputSchema,
64
- outputSchema: tool.outputSchema,
65
- annotations: tool.annotations,
66
- })),
67
- }));
57
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
58
+ const credential = await this.verifyCredential(accessToken);
59
+ return {
60
+ tools: this.registry.tools
61
+ .filter((tool) => tool.isVisibleTo(credential.listingRoles))
62
+ .map((tool) => ({
63
+ name: tool.name,
64
+ description: tool.description,
65
+ inputSchema: tool.inputSchema,
66
+ outputSchema: tool.outputSchema,
67
+ annotations: tool.annotations,
68
+ })),
69
+ };
70
+ });
68
71
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
69
72
  const tool = this.registry.find(request.params.name);
70
73
  if (!tool) {
71
74
  throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Unknown tool: ${request.params.name}`);
72
75
  }
73
- return this.call(tool, request.params.arguments ?? {}, credential);
76
+ return this.call(tool, request.params.arguments ?? {}, accessToken);
74
77
  });
75
78
  return server;
76
79
  }
80
+ async verifyCredential(accessToken) {
81
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- security boundary logs owner detail and exposes only a generic authentication failure
82
+ try {
83
+ const credential = await this.config.accessTokenAuthority.verifyAccessToken(accessToken, this.config.resource);
84
+ this.validateCredential(credential);
85
+ return credential;
86
+ }
87
+ catch (err) {
88
+ const error = (0, core_util_1.toError)(err);
89
+ log.warn('MCP access-token verification rejected a request', error);
90
+ throw new core_util_1.ApiUnauthorizedError('MCP access token rejected.', undefined, error);
91
+ }
92
+ }
77
93
  validateCredential(credential) {
94
+ const now = Math.floor(Date.now() / 1000);
95
+ if (credential.subject.trim() === '') {
96
+ throw new Error('MCP access token has no subject.');
97
+ }
98
+ if (!Number.isFinite(credential.issuedAtEpochSeconds) ||
99
+ !Number.isFinite(credential.expiresAtEpochSeconds) ||
100
+ !Number.isFinite(credential.accountValidatedAtEpochSeconds)) {
101
+ throw new Error('MCP access token security timestamps must be finite.');
102
+ }
78
103
  if (credential.resource !== this.config.resource) {
79
104
  throw new Error('MCP access token was not issued for this protected resource.');
80
105
  }
81
106
  if (!this.config.authorizationServers.includes(credential.issuer)) {
82
107
  throw new Error('MCP access token issuer is not trusted by this protected resource.');
83
108
  }
84
- if (credential.expiresAtEpochSeconds <= Date.now() / 1000) {
109
+ if (credential.issuedAtEpochSeconds > now) {
110
+ throw new Error('MCP access token was issued in the future.');
111
+ }
112
+ if (credential.expiresAtEpochSeconds <= now) {
85
113
  throw new Error('MCP access token has expired.');
86
114
  }
115
+ if (credential.expiresAtEpochSeconds - credential.issuedAtEpochSeconds >
116
+ McpAuth_1.MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS) {
117
+ throw new Error('MCP access token lifetime exceeds 30 days.');
118
+ }
87
119
  for (const scope of this.config.requiredScopes) {
88
120
  if (!credential.scopes.includes(scope)) {
89
121
  throw new Error(`MCP access token is missing required scope '${scope}'.`);
90
122
  }
91
123
  }
92
- if (credential.endpointBearerToken.trim() === '') {
93
- throw new Error('MCP verifier returned an empty endpoint bearer credential.');
124
+ if (credential.accountValidatedAtEpochSeconds > now ||
125
+ now - credential.accountValidatedAtEpochSeconds > this.config.maxAccountValidationAgeSeconds) {
126
+ throw new Error('MCP account authorization state is not fresh enough for dispatch.');
94
127
  }
95
128
  }
96
- async call(tool, args, credential) {
97
- const result = await this.dispatcher.call(tool, args ?? {}, credential.endpointBearerToken);
129
+ async call(tool, args, accessToken) {
130
+ let credential;
131
+ // webpieces-disable no-unmanaged-exceptions -- MCP transport boundary normalizes authentication failures before they reach the model
132
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- MCP transport boundary
133
+ try {
134
+ credential = await this.verifyCredential(accessToken);
135
+ }
136
+ catch (err) {
137
+ //const error = toError(err);
138
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, 'Unauthorized');
139
+ }
140
+ let endpointJwt;
141
+ // webpieces-disable no-unmanaged-exceptions -- MCP transport boundary logs mint failures and exposes only a generic protocol error
142
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- MCP transport boundary
143
+ try {
144
+ const descriptor = new McpAuth_1.McpEndpointDescriptor(tool.name, tool.apiClass.name, tool.methodName);
145
+ const mintRequest = this.config.endpointMintRequest(credential, descriptor);
146
+ endpointJwt = await this.config.endpointJwtAuthority.mint(mintRequest);
147
+ this.validateEndpointJwt(endpointJwt, accessToken);
148
+ }
149
+ catch (err) {
150
+ const error = (0, core_util_1.toError)(err);
151
+ log.error(`MCP endpoint credential mint failed for ${tool.apiClass.name}.${tool.methodName}`, error);
152
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, 'Internal Error');
153
+ }
154
+ const result = await this.dispatcher.call(tool, args ?? {}, endpointJwt.token);
98
155
  if (!result.success)
99
156
  return this.apiError(result.error, result.requestId);
100
157
  const outputFailure = this.registry.schemaBuilder.validate(tool.responseClass, result.value);
@@ -117,6 +174,19 @@ class WpMcpServer {
117
174
  structuredContent: structured,
118
175
  };
119
176
  }
177
+ validateEndpointJwt(endpointJwt, accessToken) {
178
+ const now = Math.floor(Date.now() / 1000);
179
+ if (endpointJwt.token === accessToken) {
180
+ throw new Error('MCP access-token passthrough to endpoint dispatch is forbidden.');
181
+ }
182
+ if (endpointJwt.expiresAtEpochSeconds <= now) {
183
+ throw new Error('MCP endpoint JWT has already expired.');
184
+ }
185
+ if (endpointJwt.expiresAtEpochSeconds - now >
186
+ this.config.maxEndpointJwtLifetimeSeconds) {
187
+ throw new Error('MCP endpoint JWT lifetime exceeds the configured one-hour ceiling.');
188
+ }
189
+ }
120
190
  apiError(payload, requestId) {
121
191
  return this.errorResult(new ModelVisibleToolError(payload.kind, payload.message, requestId, payload.field, payload.callerMessage, payload.errorCode, payload.retryAfterSeconds));
122
192
  }
@@ -124,7 +194,9 @@ class WpMcpServer {
124
194
  const structured = this.record(error);
125
195
  return {
126
196
  content: [{ type: 'text', text: JSON.stringify(structured) }],
127
- structuredContent: structured,
197
+ // `outputSchema` describes the endpoint's success DTO. MCP clients validate
198
+ // structuredContent against it even when isError is true, so error details must stay
199
+ // in text content until the protocol supports a separate error schema.
128
200
  isError: true,
129
201
  };
130
202
  }
@@ -1 +1 @@
1
- {"version":3,"file":"WpMcpServer.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/WpMcpServer.ts"],"names":[],"mappings":"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","sourcesContent":["import { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport {\n CallToolRequest,\n CallToolRequestSchema,\n CallToolResult,\n ErrorCode,\n ListToolsRequestSchema,\n McpError,\n} from '@modelcontextprotocol/sdk/types.js';\nimport {\n ApiErrorPayload,\n DtoValue,\n LogManager,\n toError,\n} from '@webpieces/core-util';\nimport { ApiFactory, ClassType } from '@webpieces/http-routing';\nimport { VerifiedMcpCredential, WpMcpServerConfig } from './McpAuth';\nimport { McpApiDispatcher } from './McpApiDispatcher';\nimport { McpToolRegistry, RegisteredMcpTool } from './McpToolRegistry';\n\nconst log = LogManager.getLogger('WpMcpServer');\n\n/** The only error shape rendered into model-visible MCP content. */\nexport class ModelVisibleToolError {\n constructor(\n public readonly kind: string,\n public readonly message: string,\n public readonly requestId?: string,\n public readonly field?: string,\n public readonly callerMessage?: string,\n public readonly errorCode?: string,\n public readonly retryAfterSeconds?: number,\n ) {}\n}\n\n/**\n * Node-only MCP adapter. Build one Server per authenticated transport/session; every tool call still\n * re-enters the endpoint's ordinary AuthFilter, so tools/list visibility never grants access.\n */\nexport class WpMcpServer {\n private readonly registry: McpToolRegistry;\n private readonly dispatcher: McpApiDispatcher;\n\n constructor(\n private readonly config: WpMcpServerConfig,\n apiFactory: ApiFactory,\n apiClasses: readonly ClassType[],\n ) {\n this.registry = new McpToolRegistry(apiClasses);\n this.dispatcher = new McpApiDispatcher(apiFactory);\n }\n\n async build(accessToken: string): Promise<Server> {\n const credential = await this.config.tokenVerifier.verify(\n accessToken,\n this.config.resource,\n );\n this.validateCredential(credential);\n return this.buildForCredential(credential);\n }\n\n protectedResourceMetadata(): ReturnType<WpMcpServerConfig['protectedResourceMetadata']> {\n return this.config.protectedResourceMetadata();\n }\n\n private buildForCredential(credential: VerifiedMcpCredential): Server {\n const server = new Server(\n // webpieces-disable no-anonymous-object-literals -- external MCP SDK request structure\n { name: this.config.name, version: this.config.version },\n // webpieces-disable no-anonymous-object-literals -- external MCP SDK capability structure\n { capabilities: { tools: {} } },\n );\n server.setRequestHandler(ListToolsRequestSchema, () => ({\n tools: this.registry.tools\n .filter((tool: RegisteredMcpTool) => tool.isVisibleTo(credential.listingRoles))\n .map((tool: RegisteredMcpTool) => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n outputSchema: tool.outputSchema,\n annotations: tool.annotations,\n })),\n }));\n server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {\n const tool = this.registry.find(request.params.name);\n if (!tool) {\n throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${request.params.name}`);\n }\n return this.call(tool, request.params.arguments ?? {}, credential);\n });\n return server;\n }\n\n private validateCredential(credential: VerifiedMcpCredential): void {\n if (credential.resource !== this.config.resource) {\n throw new Error('MCP access token was not issued for this protected resource.');\n }\n if (!this.config.authorizationServers.includes(credential.issuer)) {\n throw new Error('MCP access token issuer is not trusted by this protected resource.');\n }\n if (credential.expiresAtEpochSeconds <= Date.now() / 1000) {\n throw new Error('MCP access token has expired.');\n }\n for (const scope of this.config.requiredScopes) {\n if (!credential.scopes.includes(scope)) {\n throw new Error(`MCP access token is missing required scope '${scope}'.`);\n }\n }\n if (credential.endpointBearerToken.trim() === '') {\n throw new Error('MCP verifier returned an empty endpoint bearer credential.');\n }\n }\n\n private async call(\n tool: RegisteredMcpTool,\n args: DtoValue,\n credential: VerifiedMcpCredential,\n ): Promise<CallToolResult> {\n const result = await this.dispatcher.call(tool, args ?? {}, credential.endpointBearerToken);\n if (!result.success) return this.apiError(result.error, result.requestId);\n\n const outputFailure = this.registry.schemaBuilder.validate(tool.responseClass, result.value);\n if (outputFailure) {\n log.error(\n `MCP output schema violation for ${tool.apiClass.name}.${tool.methodName}: ${outputFailure.message}`,\n );\n return this.errorResult(\n new ModelVisibleToolError('implementation', 'Internal Error', result.requestId),\n );\n }\n let structured: Record<string, DtoValue>;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- transport boundary sanitizes serialization failures\n try {\n structured = this.record(result.value);\n } catch (err: unknown) {\n const error = toError(err);\n log.error(\n `MCP response serialization failed for ${tool.apiClass.name}.${tool.methodName}`,\n error,\n );\n return this.errorResult(\n new ModelVisibleToolError('implementation', 'Internal Error', result.requestId),\n );\n }\n return {\n content: [{ type: 'text', text: JSON.stringify(structured) }],\n structuredContent: structured,\n };\n }\n\n private apiError(payload: ApiErrorPayload, requestId: string): CallToolResult {\n return this.errorResult(\n new ModelVisibleToolError(\n payload.kind,\n payload.message,\n requestId,\n payload.field,\n payload.callerMessage,\n payload.errorCode,\n payload.retryAfterSeconds,\n ),\n );\n }\n\n private errorResult(error: ModelVisibleToolError): CallToolResult {\n const structured = this.record(error);\n return {\n content: [{ type: 'text', text: JSON.stringify(structured) }],\n structuredContent: structured,\n isError: true,\n };\n }\n\n private record(value: DtoValue): Record<string, DtoValue> {\n const json = JSON.stringify(value);\n if (json === undefined) throw new Error('MCP structured content is not JSON serializable.');\n const parsed = JSON.parse(json) as DtoValue;\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n // webpieces-disable no-anonymous-object-literals -- external MCP structured-content wrapper\n return { value: parsed };\n }\n return parsed as Record<string, DtoValue>;\n }\n}\n"]}
1
+ {"version":3,"file":"WpMcpServer.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/WpMcpServer.ts"],"names":[],"mappings":"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","sourcesContent":["import { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport {\n CallToolRequest,\n CallToolRequestSchema,\n CallToolResult,\n ErrorCode,\n ListToolsRequestSchema,\n McpError,\n} from '@modelcontextprotocol/sdk/types.js';\nimport {\n ApiErrorPayload,\n ApiUnauthorizedError,\n DtoValue,\n LogManager,\n toError,\n} from '@webpieces/core-util';\nimport { ApiFactory, ClassType, MintedJwt } from '@webpieces/http-routing';\nimport {\n MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS,\n McpEndpointDescriptor,\n McpProtectedResourceMetadata,\n VerifiedMcpCredential,\n WpMcpServerConfig,\n} from './McpAuth';\nimport { McpApiDispatcher } from './McpApiDispatcher';\nimport { McpToolRegistry, RegisteredMcpTool } from './McpToolRegistry';\n\nconst log = LogManager.getLogger('WpMcpServer');\n\n/** The only error shape rendered into model-visible MCP content. */\nexport class ModelVisibleToolError {\n constructor(\n public readonly kind: string,\n public readonly message: string,\n public readonly requestId?: string,\n public readonly field?: string,\n public readonly callerMessage?: string,\n public readonly errorCode?: string,\n public readonly retryAfterSeconds?: number,\n ) {}\n}\n\n/**\n * Node-only MCP adapter. Build one Server per authenticated transport/session; every tool call still\n * re-enters the endpoint's ordinary AuthFilter, so tools/list visibility never grants access.\n */\nexport class WpMcpServer<TGrant, TMintRequest> {\n private readonly registry: McpToolRegistry;\n private readonly dispatcher: McpApiDispatcher;\n\n constructor(\n private readonly config: WpMcpServerConfig<TGrant, TMintRequest>,\n apiFactory: ApiFactory,\n apiClasses: readonly ClassType[],\n ) {\n this.registry = new McpToolRegistry(apiClasses);\n this.dispatcher = new McpApiDispatcher(apiFactory);\n }\n\n async build(accessToken: string): Promise<Server> {\n await this.verifyCredential(accessToken);\n return this.buildForAccessToken(accessToken);\n }\n\n protectedResourceMetadata(): McpProtectedResourceMetadata {\n return this.config.protectedResourceMetadata();\n }\n\n private buildForAccessToken(accessToken: string): Server {\n const server = new Server(\n // webpieces-disable no-anonymous-object-literals -- external MCP SDK request structure\n { name: this.config.name, version: this.config.version },\n // webpieces-disable no-anonymous-object-literals -- external MCP SDK capability structure\n { capabilities: { tools: {} } },\n );\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n const credential = await this.verifyCredential(accessToken);\n return {\n tools: this.registry.tools\n .filter((tool: RegisteredMcpTool) => tool.isVisibleTo(credential.listingRoles))\n .map((tool: RegisteredMcpTool) => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n outputSchema: tool.outputSchema,\n annotations: tool.annotations,\n })),\n };\n });\n server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {\n const tool = this.registry.find(request.params.name);\n if (!tool) {\n throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${request.params.name}`);\n }\n return this.call(tool, request.params.arguments ?? {}, accessToken);\n });\n return server;\n }\n\n private async verifyCredential(accessToken: string): Promise<VerifiedMcpCredential> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- security boundary logs owner detail and exposes only a generic authentication failure\n try {\n const credential = await this.config.accessTokenAuthority.verifyAccessToken(\n accessToken,\n this.config.resource,\n );\n this.validateCredential(credential);\n return credential;\n } catch (err: unknown) {\n const error = toError(err);\n log.warn('MCP access-token verification rejected a request', error);\n throw new ApiUnauthorizedError('MCP access token rejected.', undefined, error);\n }\n }\n\n private validateCredential(credential: VerifiedMcpCredential): void {\n const now = Math.floor(Date.now() / 1000);\n if (credential.subject.trim() === '') {\n throw new Error('MCP access token has no subject.');\n }\n if (\n !Number.isFinite(credential.issuedAtEpochSeconds) ||\n !Number.isFinite(credential.expiresAtEpochSeconds) ||\n !Number.isFinite(credential.accountValidatedAtEpochSeconds)\n ) {\n throw new Error('MCP access token security timestamps must be finite.');\n }\n if (credential.resource !== this.config.resource) {\n throw new Error('MCP access token was not issued for this protected resource.');\n }\n if (!this.config.authorizationServers.includes(credential.issuer)) {\n throw new Error('MCP access token issuer is not trusted by this protected resource.');\n }\n if (credential.issuedAtEpochSeconds > now) {\n throw new Error('MCP access token was issued in the future.');\n }\n if (credential.expiresAtEpochSeconds <= now) {\n throw new Error('MCP access token has expired.');\n }\n if (\n credential.expiresAtEpochSeconds - credential.issuedAtEpochSeconds >\n MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS\n ) {\n throw new Error('MCP access token lifetime exceeds 30 days.');\n }\n for (const scope of this.config.requiredScopes) {\n if (!credential.scopes.includes(scope)) {\n throw new Error(`MCP access token is missing required scope '${scope}'.`);\n }\n }\n if (\n credential.accountValidatedAtEpochSeconds > now ||\n now - credential.accountValidatedAtEpochSeconds > this.config.maxAccountValidationAgeSeconds\n ) {\n throw new Error('MCP account authorization state is not fresh enough for dispatch.');\n }\n }\n\n private async call(\n tool: RegisteredMcpTool,\n args: DtoValue,\n accessToken: string,\n ): Promise<CallToolResult> {\n let credential: VerifiedMcpCredential;\n // webpieces-disable no-unmanaged-exceptions -- MCP transport boundary normalizes authentication failures before they reach the model\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- MCP transport boundary\n try {\n credential = await this.verifyCredential(accessToken);\n } catch (err: unknown) {\n //const error = toError(err);\n throw new McpError(ErrorCode.InvalidRequest, 'Unauthorized');\n }\n let endpointJwt: MintedJwt;\n // webpieces-disable no-unmanaged-exceptions -- MCP transport boundary logs mint failures and exposes only a generic protocol error\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- MCP transport boundary\n try {\n const descriptor = new McpEndpointDescriptor(\n tool.name,\n tool.apiClass.name,\n tool.methodName,\n );\n const mintRequest = this.config.endpointMintRequest(credential, descriptor);\n endpointJwt = await this.config.endpointJwtAuthority.mint(mintRequest);\n this.validateEndpointJwt(endpointJwt, accessToken);\n } catch (err: unknown) {\n const error = toError(err);\n log.error(\n `MCP endpoint credential mint failed for ${tool.apiClass.name}.${tool.methodName}`,\n error,\n );\n throw new McpError(ErrorCode.InternalError, 'Internal Error');\n }\n const result = await this.dispatcher.call(tool, args ?? {}, endpointJwt.token);\n if (!result.success) return this.apiError(result.error, result.requestId);\n\n const outputFailure = this.registry.schemaBuilder.validate(tool.responseClass, result.value);\n if (outputFailure) {\n log.error(\n `MCP output schema violation for ${tool.apiClass.name}.${tool.methodName}: ${outputFailure.message}`,\n );\n return this.errorResult(\n new ModelVisibleToolError('implementation', 'Internal Error', result.requestId),\n );\n }\n let structured: Record<string, DtoValue>;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- transport boundary sanitizes serialization failures\n try {\n structured = this.record(result.value);\n } catch (err: unknown) {\n const error = toError(err);\n log.error(\n `MCP response serialization failed for ${tool.apiClass.name}.${tool.methodName}`,\n error,\n );\n return this.errorResult(\n new ModelVisibleToolError('implementation', 'Internal Error', result.requestId),\n );\n }\n return {\n content: [{ type: 'text', text: JSON.stringify(structured) }],\n structuredContent: structured,\n };\n }\n\n private validateEndpointJwt(endpointJwt: MintedJwt, accessToken: string): void {\n const now = Math.floor(Date.now() / 1000);\n if (endpointJwt.token === accessToken) {\n throw new Error('MCP access-token passthrough to endpoint dispatch is forbidden.');\n }\n if (endpointJwt.expiresAtEpochSeconds <= now) {\n throw new Error('MCP endpoint JWT has already expired.');\n }\n if (\n endpointJwt.expiresAtEpochSeconds - now >\n this.config.maxEndpointJwtLifetimeSeconds\n ) {\n throw new Error('MCP endpoint JWT lifetime exceeds the configured one-hour ceiling.');\n }\n }\n\n private apiError(payload: ApiErrorPayload, requestId: string): CallToolResult {\n return this.errorResult(\n new ModelVisibleToolError(\n payload.kind,\n payload.message,\n requestId,\n payload.field,\n payload.callerMessage,\n payload.errorCode,\n payload.retryAfterSeconds,\n ),\n );\n }\n\n private errorResult(error: ModelVisibleToolError): CallToolResult {\n const structured = this.record(error);\n return {\n content: [{ type: 'text', text: JSON.stringify(structured) }],\n // `outputSchema` describes the endpoint's success DTO. MCP clients validate\n // structuredContent against it even when isError is true, so error details must stay\n // in text content until the protocol supports a separate error schema.\n isError: true,\n };\n }\n\n private record(value: DtoValue): Record<string, DtoValue> {\n const json = JSON.stringify(value);\n if (json === undefined) throw new Error('MCP structured content is not JSON serializable.');\n const parsed = JSON.parse(json) as DtoValue;\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n // webpieces-disable no-anonymous-object-literals -- external MCP structured-content wrapper\n return { value: parsed };\n }\n return parsed as Record<string, DtoValue>;\n }\n}\n"]}
package/src/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export { McpAccessTokenVerifier, McpProtectedResourceMetadata, VerifiedMcpCredential, WpMcpServerConfig, } from './McpAuth';
1
+ export { MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS, MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS, MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS, McpEndpointDescriptor, MintedMcpAccessToken, McpProtectedResourceMetadata, VerifiedMcpCredential, WpMcpServerConfig, } from './McpAuth';
2
+ export type { McpAccessTokenAuthority, McpEndpointMintRequestFactory } from './McpAuth';
2
3
  export { McpToolRegistry, RegisteredMcpTool } from './McpToolRegistry';
3
4
  export { McpApiDispatcher, McpDispatchFailure, McpDispatchSuccess, } from './McpApiDispatcher';
4
5
  export type { McpDispatchResult } from './McpApiDispatcher';
package/src/index.js CHANGED
@@ -1,8 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WpMcpServer = exports.ModelVisibleToolError = exports.McpDispatchSuccess = exports.McpDispatchFailure = exports.McpApiDispatcher = exports.RegisteredMcpTool = exports.McpToolRegistry = exports.WpMcpServerConfig = exports.VerifiedMcpCredential = exports.McpProtectedResourceMetadata = exports.McpAccessTokenVerifier = void 0;
3
+ exports.WpMcpServer = exports.ModelVisibleToolError = exports.McpDispatchSuccess = exports.McpDispatchFailure = exports.McpApiDispatcher = exports.RegisteredMcpTool = exports.McpToolRegistry = exports.WpMcpServerConfig = exports.VerifiedMcpCredential = exports.McpProtectedResourceMetadata = exports.MintedMcpAccessToken = exports.McpEndpointDescriptor = exports.MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS = exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS = exports.MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS = void 0;
4
4
  var McpAuth_1 = require("./McpAuth");
5
- Object.defineProperty(exports, "McpAccessTokenVerifier", { enumerable: true, get: function () { return McpAuth_1.McpAccessTokenVerifier; } });
5
+ Object.defineProperty(exports, "MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS", { enumerable: true, get: function () { return McpAuth_1.MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS; } });
6
+ Object.defineProperty(exports, "MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS", { enumerable: true, get: function () { return McpAuth_1.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS; } });
7
+ Object.defineProperty(exports, "MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS", { enumerable: true, get: function () { return McpAuth_1.MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS; } });
8
+ Object.defineProperty(exports, "McpEndpointDescriptor", { enumerable: true, get: function () { return McpAuth_1.McpEndpointDescriptor; } });
9
+ Object.defineProperty(exports, "MintedMcpAccessToken", { enumerable: true, get: function () { return McpAuth_1.MintedMcpAccessToken; } });
6
10
  Object.defineProperty(exports, "McpProtectedResourceMetadata", { enumerable: true, get: function () { return McpAuth_1.McpProtectedResourceMetadata; } });
7
11
  Object.defineProperty(exports, "VerifiedMcpCredential", { enumerable: true, get: function () { return McpAuth_1.VerifiedMcpCredential; } });
8
12
  Object.defineProperty(exports, "WpMcpServerConfig", { enumerable: true, get: function () { return McpAuth_1.WpMcpServerConfig; } });
package/src/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/index.ts"],"names":[],"mappings":";;;AAAA,qCAKmB;AAJf,iHAAA,sBAAsB,OAAA;AACtB,uHAAA,4BAA4B,OAAA;AAC5B,gHAAA,qBAAqB,OAAA;AACrB,4GAAA,iBAAiB,OAAA;AAErB,qDAAuE;AAA9D,kHAAA,eAAe,OAAA;AAAE,oHAAA,iBAAiB,OAAA;AAC3C,uDAI4B;AAHxB,oHAAA,gBAAgB,OAAA;AAChB,sHAAA,kBAAkB,OAAA;AAClB,sHAAA,kBAAkB,OAAA;AAGtB,6CAAmE;AAA1D,oHAAA,qBAAqB,OAAA;AAAE,0GAAA,WAAW,OAAA","sourcesContent":["export {\n McpAccessTokenVerifier,\n McpProtectedResourceMetadata,\n VerifiedMcpCredential,\n WpMcpServerConfig,\n} from './McpAuth';\nexport { McpToolRegistry, RegisteredMcpTool } from './McpToolRegistry';\nexport {\n McpApiDispatcher,\n McpDispatchFailure,\n McpDispatchSuccess,\n} from './McpApiDispatcher';\nexport type { McpDispatchResult } from './McpApiDispatcher';\nexport { ModelVisibleToolError, WpMcpServer } from './WpMcpServer';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/index.ts"],"names":[],"mappings":";;;AAAA,qCASmB;AARf,gIAAA,qCAAqC,OAAA;AACrC,iIAAA,sCAAsC,OAAA;AACtC,gIAAA,qCAAqC,OAAA;AACrC,gHAAA,qBAAqB,OAAA;AACrB,+GAAA,oBAAoB,OAAA;AACpB,uHAAA,4BAA4B,OAAA;AAC5B,gHAAA,qBAAqB,OAAA;AACrB,4GAAA,iBAAiB,OAAA;AAGrB,qDAAuE;AAA9D,kHAAA,eAAe,OAAA;AAAE,oHAAA,iBAAiB,OAAA;AAC3C,uDAI4B;AAHxB,oHAAA,gBAAgB,OAAA;AAChB,sHAAA,kBAAkB,OAAA;AAClB,sHAAA,kBAAkB,OAAA;AAGtB,6CAAmE;AAA1D,oHAAA,qBAAqB,OAAA;AAAE,0GAAA,WAAW,OAAA","sourcesContent":["export {\n MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS,\n MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS,\n MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS,\n McpEndpointDescriptor,\n MintedMcpAccessToken,\n McpProtectedResourceMetadata,\n VerifiedMcpCredential,\n WpMcpServerConfig,\n} from './McpAuth';\nexport type { McpAccessTokenAuthority, McpEndpointMintRequestFactory } from './McpAuth';\nexport { McpToolRegistry, RegisteredMcpTool } from './McpToolRegistry';\nexport {\n McpApiDispatcher,\n McpDispatchFailure,\n McpDispatchSuccess,\n} from './McpApiDispatcher';\nexport type { McpDispatchResult } from './McpApiDispatcher';\nexport { ModelVisibleToolError, WpMcpServer } from './WpMcpServer';\n"]}