@webpieces/mcp-server 0.4.765
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 +51 -0
- package/package.json +32 -0
- package/src/McpApiDispatcher.d.ts +23 -0
- package/src/McpApiDispatcher.js +58 -0
- package/src/McpApiDispatcher.js.map +1 -0
- package/src/McpAuth.d.ts +42 -0
- package/src/McpAuth.js +69 -0
- package/src/McpAuth.js.map +1 -0
- package/src/McpToolRegistry.d.ts +26 -0
- package/src/McpToolRegistry.js +80 -0
- package/src/McpToolRegistry.js.map +1 -0
- package/src/WpMcpServer.d.ts +32 -0
- package/src/WpMcpServer.js +144 -0
- package/src/WpMcpServer.js.map +1 -0
- package/src/index.d.ts +5 -0
- package/src/index.js +19 -0
- package/src/index.js.map +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @webpieces/mcp-server
|
|
2
|
+
|
|
3
|
+
Publishes explicitly annotated Webpieces RPC endpoints as MCP tools. The bridge generates input and
|
|
4
|
+
output JSON Schema from DTO metadata, invokes the normal Webpieces filter/controller path, and turns
|
|
5
|
+
exceptions into safe model-visible MCP results.
|
|
6
|
+
|
|
7
|
+
`@WpMcpTool` is an opt-in. Existing endpoint authentication remains authoritative on every call.
|
|
8
|
+
The MCP adapter never accepts trusted context values from tool arguments or headers.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
@WpDto()
|
|
12
|
+
class FindOrderRequest {
|
|
13
|
+
@WpDtoField(new WpDtoFieldOptions('Order identifier', true))
|
|
14
|
+
orderId!: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@WpDto()
|
|
18
|
+
class FindOrderResponse {
|
|
19
|
+
@WpDtoField(new WpDtoFieldOptions('Current order state', true))
|
|
20
|
+
state!: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@ApiPath('/orders')
|
|
24
|
+
abstract class OrdersApi {
|
|
25
|
+
@WpAuthJwt({ allRolesAllowed: true })
|
|
26
|
+
@Endpoint('/find', 'rpc')
|
|
27
|
+
@WpResponseDto(() => FindOrderResponse)
|
|
28
|
+
@WpMcpTool({
|
|
29
|
+
name: 'orders_find',
|
|
30
|
+
description: 'Find one order owned by the signed-in user.',
|
|
31
|
+
readOnlyHint: true,
|
|
32
|
+
idempotentHint: true,
|
|
33
|
+
openWorldHint: false,
|
|
34
|
+
})
|
|
35
|
+
find(request: FindOrderRequest): Promise<FindOrderResponse> {
|
|
36
|
+
throw new Error('contract only');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The `@WpMcpTool.description` becomes the tool description returned by `tools/list`. The request and
|
|
42
|
+
response classes generate `inputSchema` and `outputSchema`; `@WpDtoField` supplies property
|
|
43
|
+
descriptions and the facts TypeScript erases, such as optionality and array element types.
|
|
44
|
+
|
|
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.
|
|
49
|
+
|
|
50
|
+
`protectedResourceMetadata()` returns the resource metadata an HTTP adapter can publish at the
|
|
51
|
+
well-known OAuth protected-resource endpoint. OAuth token issuance remains pluggable.
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webpieces/mcp-server",
|
|
3
|
+
"version": "0.4.765",
|
|
4
|
+
"description": "Secure MCP tool generation from annotated Webpieces API contracts",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"author": "Dean Hiller",
|
|
9
|
+
"license": "Apache-2.0",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/deanhiller/webpieces-ts.git",
|
|
13
|
+
"directory": "packages/http/mcp-server"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"webpieces",
|
|
17
|
+
"mcp",
|
|
18
|
+
"server",
|
|
19
|
+
"ai"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
26
|
+
"@webpieces/core-context": "0.4.765",
|
|
27
|
+
"@webpieces/core-util": "0.4.765",
|
|
28
|
+
"@webpieces/http-routing": "0.4.765",
|
|
29
|
+
"reflect-metadata": "0.2.2",
|
|
30
|
+
"tslib": "2.8.1"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ApiErrorPayload, DtoValue } from '@webpieces/core-util';
|
|
2
|
+
import { ApiFactory } from '@webpieces/http-routing';
|
|
3
|
+
import { RegisteredMcpTool } from './McpToolRegistry';
|
|
4
|
+
export declare class McpDispatchSuccess {
|
|
5
|
+
readonly value: DtoValue;
|
|
6
|
+
readonly requestId: string;
|
|
7
|
+
readonly success = true;
|
|
8
|
+
constructor(value: DtoValue, requestId: string);
|
|
9
|
+
}
|
|
10
|
+
export declare class McpDispatchFailure {
|
|
11
|
+
readonly error: ApiErrorPayload;
|
|
12
|
+
readonly requestId: string;
|
|
13
|
+
readonly success = false;
|
|
14
|
+
constructor(error: ApiErrorPayload, requestId: string);
|
|
15
|
+
}
|
|
16
|
+
export type McpDispatchResult = McpDispatchSuccess | McpDispatchFailure;
|
|
17
|
+
/** Executes one tool through the same ApiFactory proxy/filter/controller path as HTTP. */
|
|
18
|
+
export declare class McpApiDispatcher {
|
|
19
|
+
private readonly apiFactory;
|
|
20
|
+
private readonly schemaBuilder;
|
|
21
|
+
constructor(apiFactory: ApiFactory);
|
|
22
|
+
call(tool: RegisteredMcpTool, requestDto: DtoValue, endpointBearerToken: string): Promise<McpDispatchResult>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpApiDispatcher = exports.McpDispatchFailure = exports.McpDispatchSuccess = void 0;
|
|
4
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
5
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
6
|
+
class McpDispatchSuccess {
|
|
7
|
+
value;
|
|
8
|
+
requestId;
|
|
9
|
+
success = true;
|
|
10
|
+
constructor(value, requestId) {
|
|
11
|
+
this.value = value;
|
|
12
|
+
this.requestId = requestId;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.McpDispatchSuccess = McpDispatchSuccess;
|
|
16
|
+
class McpDispatchFailure {
|
|
17
|
+
error;
|
|
18
|
+
requestId;
|
|
19
|
+
success = false;
|
|
20
|
+
constructor(error, requestId) {
|
|
21
|
+
this.error = error;
|
|
22
|
+
this.requestId = requestId;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.McpDispatchFailure = McpDispatchFailure;
|
|
26
|
+
/** Executes one tool through the same ApiFactory proxy/filter/controller path as HTTP. */
|
|
27
|
+
class McpApiDispatcher {
|
|
28
|
+
apiFactory;
|
|
29
|
+
schemaBuilder = new core_util_1.DtoSchemaBuilder();
|
|
30
|
+
constructor(apiFactory) {
|
|
31
|
+
this.apiFactory = apiFactory;
|
|
32
|
+
}
|
|
33
|
+
async call(tool, requestDto, endpointBearerToken) {
|
|
34
|
+
const headers = new Map();
|
|
35
|
+
headers.set('authorization', [`Bearer ${endpointBearerToken}`]);
|
|
36
|
+
const request = new core_context_1.HttpRequest('POST', `/__webpieces/mcp/${tool.name}`, headers);
|
|
37
|
+
return core_context_1.RequestContext.run(async () => {
|
|
38
|
+
new core_context_1.RequestContextHeaders().fillFromRequest(request);
|
|
39
|
+
const requestId = core_context_1.RequestContext.getUntrusted(core_util_1.WebpiecesCoreHeaders.REQUEST_ID) ?? 'missing-request-id';
|
|
40
|
+
const inputFailure = this.schemaBuilder.validate(tool.requestClass, requestDto);
|
|
41
|
+
if (inputFailure) {
|
|
42
|
+
return new McpDispatchFailure(core_util_1.ApiErrorCodec.encode(new core_util_1.ApiBadRequestError('MCP request DTO did not match its declared schema.', undefined, inputFailure.message)), requestId);
|
|
43
|
+
}
|
|
44
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- transport boundary sanitizes endpoint throws
|
|
45
|
+
try {
|
|
46
|
+
const client = this.apiFactory.createApiClient(tool.apiClass);
|
|
47
|
+
const value = (await client[tool.methodName](requestDto));
|
|
48
|
+
return new McpDispatchSuccess(value, requestId);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
const error = (0, core_util_1.toError)(err);
|
|
52
|
+
return new McpDispatchFailure(core_util_1.ApiErrorCodec.encode(error), requestId);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.McpApiDispatcher = McpApiDispatcher;
|
|
58
|
+
//# sourceMappingURL=McpApiDispatcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"McpApiDispatcher.js","sourceRoot":"","sources":["../../../../../../packages/http/mcp-server/src/McpApiDispatcher.ts"],"names":[],"mappings":";;;AAAA,oDAQ8B;AAC9B,0DAA6F;AAI7F,MAAa,kBAAkB;IAEC;IAAiC;IADpD,OAAO,GAAG,IAAI,CAAC;IACxB,YAA4B,KAAe,EAAkB,SAAiB;QAAlD,UAAK,GAAL,KAAK,CAAU;QAAkB,cAAS,GAAT,SAAS,CAAQ;IAAG,CAAC;CACrF;AAHD,gDAGC;AAED,MAAa,kBAAkB;IAEC;IAAwC;IAD3D,OAAO,GAAG,KAAK,CAAC;IACzB,YAA4B,KAAsB,EAAkB,SAAiB;QAAzD,UAAK,GAAL,KAAK,CAAiB;QAAkB,cAAS,GAAT,SAAS,CAAQ;IAAG,CAAC;CAC5F;AAHD,gDAGC;AAID,0FAA0F;AAC1F,MAAa,gBAAgB;IAGI;IAFZ,aAAa,GAAG,IAAI,4BAAgB,EAAE,CAAC;IAExD,YAA6B,UAAsB;QAAtB,eAAU,GAAV,UAAU,CAAY;IAAG,CAAC;IAEvD,KAAK,CAAC,IAAI,CACN,IAAuB,EACvB,UAAoB,EACpB,mBAA2B;QAE3B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAoB,CAAC;QAC5C,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,CAAC,UAAU,mBAAmB,EAAE,CAAC,CAAC,CAAC;QAChE,MAAM,OAAO,GAAG,IAAI,0BAAW,CAAC,MAAM,EAAE,oBAAoB,IAAI,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;QAClF,OAAO,6BAAc,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE;YACjC,IAAI,oCAAqB,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;YACrD,MAAM,SAAS,GACX,6BAAc,CAAC,YAAY,CAAC,gCAAoB,CAAC,UAAU,CAAC,IAAI,oBAAoB,CAAC;YACzF,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;YAChF,IAAI,YAAY,EAAE,CAAC;gBACf,OAAO,IAAI,kBAAkB,CACzB,yBAAa,CAAC,MAAM,CAChB,IAAI,8BAAkB,CAClB,oDAAoD,EACpD,SAAS,EACT,YAAY,CAAC,OAAO,CACvB,CACJ,EACD,SAAS,CACZ,CAAC;YACN,CAAC;YACD,8GAA8G;YAC9G,IAAI,CAAC;gBACD,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,eAAe,CAAiB,IAAI,CAAC,QAAiB,CAAC,CAAC;gBACvF,MAAM,KAAK,GAAG,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,UAAU,CAAC,CAAa,CAAC;gBACtE,OAAO,IAAI,kBAAkB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;YACpD,CAAC;YAAC,OAAO,GAAY,EAAE,CAAC;gBACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;gBAC3B,OAAO,IAAI,kBAAkB,CAAC,yBAAa,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,CAAC;YAC1E,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC;CACJ;AAzCD,4CAyCC","sourcesContent":["import {\n ApiBadRequestError,\n ApiErrorCodec,\n ApiErrorPayload,\n DtoSchemaBuilder,\n DtoValue,\n toError,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { HttpRequest, RequestContext, RequestContextHeaders } from '@webpieces/core-context';\nimport { ApiFactory, ApiClientProxy } from '@webpieces/http-routing';\nimport { RegisteredMcpTool } from './McpToolRegistry';\n\nexport class McpDispatchSuccess {\n readonly success = true;\n constructor(public readonly value: DtoValue, public readonly requestId: string) {}\n}\n\nexport class McpDispatchFailure {\n readonly success = false;\n constructor(public readonly error: ApiErrorPayload, public readonly requestId: string) {}\n}\n\nexport type McpDispatchResult = McpDispatchSuccess | McpDispatchFailure;\n\n/** Executes one tool through the same ApiFactory proxy/filter/controller path as HTTP. */\nexport class McpApiDispatcher {\n private readonly schemaBuilder = new DtoSchemaBuilder();\n\n constructor(private readonly apiFactory: ApiFactory) {}\n\n async call(\n tool: RegisteredMcpTool,\n requestDto: DtoValue,\n endpointBearerToken: string,\n ): Promise<McpDispatchResult> {\n const headers = new Map<string, string[]>();\n headers.set('authorization', [`Bearer ${endpointBearerToken}`]);\n const request = new HttpRequest('POST', `/__webpieces/mcp/${tool.name}`, headers);\n return RequestContext.run(async () => {\n new RequestContextHeaders().fillFromRequest(request);\n const requestId =\n RequestContext.getUntrusted(WebpiecesCoreHeaders.REQUEST_ID) ?? 'missing-request-id';\n const inputFailure = this.schemaBuilder.validate(tool.requestClass, requestDto);\n if (inputFailure) {\n return new McpDispatchFailure(\n ApiErrorCodec.encode(\n new ApiBadRequestError(\n 'MCP request DTO did not match its declared schema.',\n undefined,\n inputFailure.message,\n ),\n ),\n requestId,\n );\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- transport boundary sanitizes endpoint throws\n try {\n const client = this.apiFactory.createApiClient<ApiClientProxy>(tool.apiClass as never);\n const value = (await client[tool.methodName](requestDto)) as DtoValue;\n return new McpDispatchSuccess(value, requestId);\n } catch (err: unknown) {\n const error = toError(err);\n return new McpDispatchFailure(ApiErrorCodec.encode(error), requestId);\n }\n });\n }\n}\n"]}
|
package/src/McpAuth.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Credential accepted at the MCP resource boundary and the local endpoint credential it resolves to. */
|
|
2
|
+
export declare class VerifiedMcpCredential {
|
|
3
|
+
/** Used only on the in-process endpoint invocation; never forwarded to another service. */
|
|
4
|
+
readonly endpointBearerToken: string;
|
|
5
|
+
readonly issuer: string;
|
|
6
|
+
/** The RFC 8707 resource/audience the verifier proved from the access token. */
|
|
7
|
+
readonly resource: string;
|
|
8
|
+
readonly expiresAtEpochSeconds: number;
|
|
9
|
+
readonly scopes: readonly string[];
|
|
10
|
+
/** Advisory tools/list filtering only. Endpoint authorization always runs again. */
|
|
11
|
+
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[],
|
|
17
|
+
/** Advisory tools/list filtering only. Endpoint authorization always runs again. */
|
|
18
|
+
listingRoles?: readonly string[]);
|
|
19
|
+
}
|
|
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>;
|
|
23
|
+
}
|
|
24
|
+
/** RFC 9728-style metadata exposed by the MCP protected resource. */
|
|
25
|
+
export declare class McpProtectedResourceMetadata {
|
|
26
|
+
readonly resource: string;
|
|
27
|
+
readonly authorization_servers: readonly string[];
|
|
28
|
+
readonly bearer_methods_supported: string[];
|
|
29
|
+
readonly scopes_supported?: readonly string[];
|
|
30
|
+
constructor(resource: string, authorizationServers: readonly string[], scopesSupported?: readonly string[]);
|
|
31
|
+
}
|
|
32
|
+
/** Node MCP server configuration. OAuth token issuance stays with the application/identity provider. */
|
|
33
|
+
export declare class WpMcpServerConfig {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly version: string;
|
|
36
|
+
readonly resource: string;
|
|
37
|
+
readonly tokenVerifier: McpAccessTokenVerifier;
|
|
38
|
+
readonly authorizationServers: readonly string[];
|
|
39
|
+
readonly requiredScopes: readonly string[];
|
|
40
|
+
constructor(name: string, version: string, resource: string, tokenVerifier: McpAccessTokenVerifier, authorizationServers: readonly string[], requiredScopes: readonly string[]);
|
|
41
|
+
protectedResourceMetadata(): McpProtectedResourceMetadata;
|
|
42
|
+
}
|
package/src/McpAuth.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
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. */
|
|
5
|
+
class VerifiedMcpCredential {
|
|
6
|
+
endpointBearerToken;
|
|
7
|
+
issuer;
|
|
8
|
+
resource;
|
|
9
|
+
expiresAtEpochSeconds;
|
|
10
|
+
scopes;
|
|
11
|
+
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,
|
|
17
|
+
/** Advisory tools/list filtering only. Endpoint authorization always runs again. */
|
|
18
|
+
listingRoles = []) {
|
|
19
|
+
this.endpointBearerToken = endpointBearerToken;
|
|
20
|
+
this.issuer = issuer;
|
|
21
|
+
this.resource = resource;
|
|
22
|
+
this.expiresAtEpochSeconds = expiresAtEpochSeconds;
|
|
23
|
+
this.scopes = scopes;
|
|
24
|
+
this.listingRoles = listingRoles;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.VerifiedMcpCredential = VerifiedMcpCredential;
|
|
28
|
+
/** Application-owned verification/token-exchange seam for a resource-bound MCP access token. */
|
|
29
|
+
class McpAccessTokenVerifier {
|
|
30
|
+
}
|
|
31
|
+
exports.McpAccessTokenVerifier = McpAccessTokenVerifier;
|
|
32
|
+
/** RFC 9728-style metadata exposed by the MCP protected resource. */
|
|
33
|
+
class McpProtectedResourceMetadata {
|
|
34
|
+
resource;
|
|
35
|
+
authorization_servers;
|
|
36
|
+
bearer_methods_supported = ['header'];
|
|
37
|
+
scopes_supported;
|
|
38
|
+
constructor(resource, authorizationServers, scopesSupported) {
|
|
39
|
+
this.resource = resource;
|
|
40
|
+
this.authorization_servers = authorizationServers;
|
|
41
|
+
this.scopes_supported = scopesSupported;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.McpProtectedResourceMetadata = McpProtectedResourceMetadata;
|
|
45
|
+
/** Node MCP server configuration. OAuth token issuance stays with the application/identity provider. */
|
|
46
|
+
class WpMcpServerConfig {
|
|
47
|
+
name;
|
|
48
|
+
version;
|
|
49
|
+
resource;
|
|
50
|
+
tokenVerifier;
|
|
51
|
+
authorizationServers;
|
|
52
|
+
requiredScopes;
|
|
53
|
+
constructor(name, version, resource, tokenVerifier, authorizationServers, requiredScopes) {
|
|
54
|
+
this.name = name;
|
|
55
|
+
this.version = version;
|
|
56
|
+
this.resource = resource;
|
|
57
|
+
this.tokenVerifier = tokenVerifier;
|
|
58
|
+
this.authorizationServers = authorizationServers;
|
|
59
|
+
this.requiredScopes = requiredScopes;
|
|
60
|
+
if (name.trim() === '' || version.trim() === '' || resource.trim() === '') {
|
|
61
|
+
throw new Error('MCP name, version, and resource must be non-empty.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
protectedResourceMetadata() {
|
|
65
|
+
return new McpProtectedResourceMetadata(this.resource, this.authorizationServers, this.requiredScopes);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.WpMcpServerConfig = WpMcpServerConfig;
|
|
69
|
+
//# sourceMappingURL=McpAuth.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ApiJsonSchema, AuthMeta, DtoClass, DtoSchemaBuilder, WpMcpToolMetadata } from '@webpieces/core-util';
|
|
2
|
+
import { ClassType } from '@webpieces/http-routing';
|
|
3
|
+
/** Fully resolved contract metadata for one MCP tool. */
|
|
4
|
+
export declare class RegisteredMcpTool {
|
|
5
|
+
readonly apiClass: ClassType;
|
|
6
|
+
readonly methodName: string;
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly description: string;
|
|
9
|
+
readonly annotations: WpMcpToolMetadata['hints'];
|
|
10
|
+
readonly authMeta: AuthMeta;
|
|
11
|
+
readonly requestClass: DtoClass;
|
|
12
|
+
readonly responseClass: DtoClass;
|
|
13
|
+
readonly inputSchema: ApiJsonSchema;
|
|
14
|
+
readonly outputSchema: ApiJsonSchema;
|
|
15
|
+
constructor(apiClass: ClassType, methodName: string, name: string, description: string, annotations: WpMcpToolMetadata['hints'], authMeta: AuthMeta, requestClass: DtoClass, responseClass: DtoClass, inputSchema: ApiJsonSchema, outputSchema: ApiJsonSchema);
|
|
16
|
+
/** Listing is advisory; the ordinary endpoint AuthFilter is the only authorization boundary. */
|
|
17
|
+
isVisibleTo(listingRoles: readonly string[]): boolean;
|
|
18
|
+
}
|
|
19
|
+
/** Fail-fast registry built entirely from API/decorator metadata. */
|
|
20
|
+
export declare class McpToolRegistry {
|
|
21
|
+
readonly tools: readonly RegisteredMcpTool[];
|
|
22
|
+
readonly schemaBuilder: DtoSchemaBuilder;
|
|
23
|
+
constructor(apiClasses: readonly ClassType[]);
|
|
24
|
+
find(name: string): RegisteredMcpTool | undefined;
|
|
25
|
+
private resolve;
|
|
26
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpToolRegistry = exports.RegisteredMcpTool = void 0;
|
|
4
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
5
|
+
/** Fully resolved contract metadata for one MCP tool. */
|
|
6
|
+
class RegisteredMcpTool {
|
|
7
|
+
apiClass;
|
|
8
|
+
methodName;
|
|
9
|
+
name;
|
|
10
|
+
description;
|
|
11
|
+
annotations;
|
|
12
|
+
authMeta;
|
|
13
|
+
requestClass;
|
|
14
|
+
responseClass;
|
|
15
|
+
inputSchema;
|
|
16
|
+
outputSchema;
|
|
17
|
+
constructor(apiClass, methodName, name, description, annotations, authMeta, requestClass, responseClass, inputSchema, outputSchema) {
|
|
18
|
+
this.apiClass = apiClass;
|
|
19
|
+
this.methodName = methodName;
|
|
20
|
+
this.name = name;
|
|
21
|
+
this.description = description;
|
|
22
|
+
this.annotations = annotations;
|
|
23
|
+
this.authMeta = authMeta;
|
|
24
|
+
this.requestClass = requestClass;
|
|
25
|
+
this.responseClass = responseClass;
|
|
26
|
+
this.inputSchema = inputSchema;
|
|
27
|
+
this.outputSchema = outputSchema;
|
|
28
|
+
}
|
|
29
|
+
/** Listing is advisory; the ordinary endpoint AuthFilter is the only authorization boundary. */
|
|
30
|
+
isVisibleTo(listingRoles) {
|
|
31
|
+
if (this.authMeta.mode.kind !== 'jwt')
|
|
32
|
+
return true;
|
|
33
|
+
const required = (0, core_util_1.rolesRequired)(this.authMeta.mode.requirement);
|
|
34
|
+
return required.length === 0 || required.some((role) => listingRoles.includes(role));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.RegisteredMcpTool = RegisteredMcpTool;
|
|
38
|
+
/** Fail-fast registry built entirely from API/decorator metadata. */
|
|
39
|
+
class McpToolRegistry {
|
|
40
|
+
tools;
|
|
41
|
+
schemaBuilder = new core_util_1.DtoSchemaBuilder();
|
|
42
|
+
constructor(apiClasses) {
|
|
43
|
+
const registered = [];
|
|
44
|
+
const names = new Set();
|
|
45
|
+
for (const apiClass of apiClasses) {
|
|
46
|
+
for (const metadata of (0, core_util_1.getWpMcpTools)(apiClass)) {
|
|
47
|
+
if (names.has(metadata.name)) {
|
|
48
|
+
throw new Error(`Duplicate @WpMcpTool name '${metadata.name}'. Tool names must be globally unique.`);
|
|
49
|
+
}
|
|
50
|
+
names.add(metadata.name);
|
|
51
|
+
registered.push(this.resolve(apiClass, metadata));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
this.tools = registered;
|
|
55
|
+
}
|
|
56
|
+
find(name) {
|
|
57
|
+
return this.tools.find((tool) => tool.name === name);
|
|
58
|
+
}
|
|
59
|
+
resolve(apiClass, metadata) {
|
|
60
|
+
const endpoints = (0, core_util_1.getEndpoints)(apiClass) ?? {};
|
|
61
|
+
if (!endpoints[metadata.methodName]) {
|
|
62
|
+
throw new Error(`@WpMcpTool ${apiClass.name}.${metadata.methodName} must also be an @Endpoint.`);
|
|
63
|
+
}
|
|
64
|
+
if ((0, core_util_1.getEndpointKind)(apiClass, metadata.methodName) !== 'rpc') {
|
|
65
|
+
throw new Error(`@WpMcpTool ${apiClass.name}.${metadata.methodName} must be an RPC endpoint.`);
|
|
66
|
+
}
|
|
67
|
+
const authMeta = (0, core_util_1.getAuthMeta)(apiClass, metadata.methodName);
|
|
68
|
+
if (!authMeta || (authMeta.mode.kind !== 'jwt' && authMeta.mode.kind !== 'public')) {
|
|
69
|
+
throw new Error(`@WpMcpTool ${apiClass.name}.${metadata.methodName} must use @WpAuthJwt or @WpAuthPublic.`);
|
|
70
|
+
}
|
|
71
|
+
if (authMeta.mode.kind === 'public' && !metadata.hints.readOnlyHint) {
|
|
72
|
+
throw new Error(`Public MCP tool ${metadata.name} must be read-only.`);
|
|
73
|
+
}
|
|
74
|
+
const requestClass = this.schemaBuilder.requestClassOf(apiClass, metadata.methodName);
|
|
75
|
+
const responseClass = this.schemaBuilder.responseClassOf(apiClass, metadata.methodName);
|
|
76
|
+
return new RegisteredMcpTool(apiClass, metadata.methodName, metadata.name, metadata.description, metadata.hints, authMeta, requestClass, responseClass, this.schemaBuilder.build(requestClass), this.schemaBuilder.build(responseClass));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.McpToolRegistry = McpToolRegistry;
|
|
80
|
+
//# sourceMappingURL=McpToolRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"McpToolRegistry.js","sourceRoot":"","sources":["../../../../../../packages/http/mcp-server/src/McpToolRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ApiJsonSchema,\n AuthMeta,\n DtoClass,\n DtoSchemaBuilder,\n getAuthMeta,\n getEndpointKind,\n getEndpoints,\n getWpMcpTools,\n rolesRequired,\n WpMcpToolMetadata,\n} from '@webpieces/core-util';\nimport { ClassType } from '@webpieces/http-routing';\n\n/** Fully resolved contract metadata for one MCP tool. */\nexport class RegisteredMcpTool {\n constructor(\n public readonly apiClass: ClassType,\n public readonly methodName: string,\n public readonly name: string,\n public readonly description: string,\n public readonly annotations: WpMcpToolMetadata['hints'],\n public readonly authMeta: AuthMeta,\n public readonly requestClass: DtoClass,\n public readonly responseClass: DtoClass,\n public readonly inputSchema: ApiJsonSchema,\n public readonly outputSchema: ApiJsonSchema,\n ) {}\n\n /** Listing is advisory; the ordinary endpoint AuthFilter is the only authorization boundary. */\n isVisibleTo(listingRoles: readonly string[]): boolean {\n if (this.authMeta.mode.kind !== 'jwt') return true;\n const required = rolesRequired(this.authMeta.mode.requirement);\n return required.length === 0 || required.some((role: string) => listingRoles.includes(role));\n }\n}\n\n/** Fail-fast registry built entirely from API/decorator metadata. */\nexport class McpToolRegistry {\n readonly tools: readonly RegisteredMcpTool[];\n readonly schemaBuilder = new DtoSchemaBuilder();\n\n constructor(apiClasses: readonly ClassType[]) {\n const registered: RegisteredMcpTool[] = [];\n const names = new Set<string>();\n for (const apiClass of apiClasses) {\n for (const metadata of getWpMcpTools(apiClass)) {\n if (names.has(metadata.name)) {\n throw new Error(`Duplicate @WpMcpTool name '${metadata.name}'. Tool names must be globally unique.`);\n }\n names.add(metadata.name);\n registered.push(this.resolve(apiClass, metadata));\n }\n }\n this.tools = registered;\n }\n\n find(name: string): RegisteredMcpTool | undefined {\n return this.tools.find((tool: RegisteredMcpTool) => tool.name === name);\n }\n\n private resolve(apiClass: ClassType, metadata: WpMcpToolMetadata): RegisteredMcpTool {\n const endpoints = getEndpoints(apiClass) ?? {};\n if (!endpoints[metadata.methodName]) {\n throw new Error(`@WpMcpTool ${apiClass.name}.${metadata.methodName} must also be an @Endpoint.`);\n }\n if (getEndpointKind(apiClass, metadata.methodName) !== 'rpc') {\n throw new Error(`@WpMcpTool ${apiClass.name}.${metadata.methodName} must be an RPC endpoint.`);\n }\n const authMeta = getAuthMeta(apiClass, metadata.methodName);\n if (!authMeta || (authMeta.mode.kind !== 'jwt' && authMeta.mode.kind !== 'public')) {\n throw new Error(\n `@WpMcpTool ${apiClass.name}.${metadata.methodName} must use @WpAuthJwt or @WpAuthPublic.`,\n );\n }\n if (authMeta.mode.kind === 'public' && !metadata.hints.readOnlyHint) {\n throw new Error(`Public MCP tool ${metadata.name} must be read-only.`);\n }\n const requestClass = this.schemaBuilder.requestClassOf(apiClass, metadata.methodName);\n const responseClass = this.schemaBuilder.responseClassOf(apiClass, metadata.methodName);\n return new RegisteredMcpTool(\n apiClass,\n metadata.methodName,\n metadata.name,\n metadata.description,\n metadata.hints,\n authMeta,\n requestClass,\n responseClass,\n this.schemaBuilder.build(requestClass),\n this.schemaBuilder.build(responseClass),\n );\n }\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { ApiFactory, ClassType } from '@webpieces/http-routing';
|
|
3
|
+
import { WpMcpServerConfig } from './McpAuth';
|
|
4
|
+
/** The only error shape rendered into model-visible MCP content. */
|
|
5
|
+
export declare class ModelVisibleToolError {
|
|
6
|
+
readonly kind: string;
|
|
7
|
+
readonly message: string;
|
|
8
|
+
readonly requestId?: string | undefined;
|
|
9
|
+
readonly field?: string | undefined;
|
|
10
|
+
readonly callerMessage?: string | undefined;
|
|
11
|
+
readonly errorCode?: string | undefined;
|
|
12
|
+
readonly retryAfterSeconds?: number | undefined;
|
|
13
|
+
constructor(kind: string, message: string, requestId?: string | undefined, field?: string | undefined, callerMessage?: string | undefined, errorCode?: string | undefined, retryAfterSeconds?: number | undefined);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Node-only MCP adapter. Build one Server per authenticated transport/session; every tool call still
|
|
17
|
+
* re-enters the endpoint's ordinary AuthFilter, so tools/list visibility never grants access.
|
|
18
|
+
*/
|
|
19
|
+
export declare class WpMcpServer {
|
|
20
|
+
private readonly config;
|
|
21
|
+
private readonly registry;
|
|
22
|
+
private readonly dispatcher;
|
|
23
|
+
constructor(config: WpMcpServerConfig, apiFactory: ApiFactory, apiClasses: readonly ClassType[]);
|
|
24
|
+
build(accessToken: string): Promise<Server>;
|
|
25
|
+
protectedResourceMetadata(): ReturnType<WpMcpServerConfig['protectedResourceMetadata']>;
|
|
26
|
+
private buildForCredential;
|
|
27
|
+
private validateCredential;
|
|
28
|
+
private call;
|
|
29
|
+
private apiError;
|
|
30
|
+
private errorResult;
|
|
31
|
+
private record;
|
|
32
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WpMcpServer = exports.ModelVisibleToolError = void 0;
|
|
4
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
6
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
7
|
+
const McpApiDispatcher_1 = require("./McpApiDispatcher");
|
|
8
|
+
const McpToolRegistry_1 = require("./McpToolRegistry");
|
|
9
|
+
const log = core_util_1.LogManager.getLogger('WpMcpServer');
|
|
10
|
+
/** The only error shape rendered into model-visible MCP content. */
|
|
11
|
+
class ModelVisibleToolError {
|
|
12
|
+
kind;
|
|
13
|
+
message;
|
|
14
|
+
requestId;
|
|
15
|
+
field;
|
|
16
|
+
callerMessage;
|
|
17
|
+
errorCode;
|
|
18
|
+
retryAfterSeconds;
|
|
19
|
+
constructor(kind, message, requestId, field, callerMessage, errorCode, retryAfterSeconds) {
|
|
20
|
+
this.kind = kind;
|
|
21
|
+
this.message = message;
|
|
22
|
+
this.requestId = requestId;
|
|
23
|
+
this.field = field;
|
|
24
|
+
this.callerMessage = callerMessage;
|
|
25
|
+
this.errorCode = errorCode;
|
|
26
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.ModelVisibleToolError = ModelVisibleToolError;
|
|
30
|
+
/**
|
|
31
|
+
* Node-only MCP adapter. Build one Server per authenticated transport/session; every tool call still
|
|
32
|
+
* re-enters the endpoint's ordinary AuthFilter, so tools/list visibility never grants access.
|
|
33
|
+
*/
|
|
34
|
+
class WpMcpServer {
|
|
35
|
+
config;
|
|
36
|
+
registry;
|
|
37
|
+
dispatcher;
|
|
38
|
+
constructor(config, apiFactory, apiClasses) {
|
|
39
|
+
this.config = config;
|
|
40
|
+
this.registry = new McpToolRegistry_1.McpToolRegistry(apiClasses);
|
|
41
|
+
this.dispatcher = new McpApiDispatcher_1.McpApiDispatcher(apiFactory);
|
|
42
|
+
}
|
|
43
|
+
async build(accessToken) {
|
|
44
|
+
const credential = await this.config.tokenVerifier.verify(accessToken, this.config.resource);
|
|
45
|
+
this.validateCredential(credential);
|
|
46
|
+
return this.buildForCredential(credential);
|
|
47
|
+
}
|
|
48
|
+
protectedResourceMetadata() {
|
|
49
|
+
return this.config.protectedResourceMetadata();
|
|
50
|
+
}
|
|
51
|
+
buildForCredential(credential) {
|
|
52
|
+
const server = new index_js_1.Server(
|
|
53
|
+
// webpieces-disable no-anonymous-object-literals -- external MCP SDK request structure
|
|
54
|
+
{ name: this.config.name, version: this.config.version },
|
|
55
|
+
// webpieces-disable no-anonymous-object-literals -- external MCP SDK capability structure
|
|
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
|
+
}));
|
|
68
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
69
|
+
const tool = this.registry.find(request.params.name);
|
|
70
|
+
if (!tool) {
|
|
71
|
+
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Unknown tool: ${request.params.name}`);
|
|
72
|
+
}
|
|
73
|
+
return this.call(tool, request.params.arguments ?? {}, credential);
|
|
74
|
+
});
|
|
75
|
+
return server;
|
|
76
|
+
}
|
|
77
|
+
validateCredential(credential) {
|
|
78
|
+
if (credential.resource !== this.config.resource) {
|
|
79
|
+
throw new Error('MCP access token was not issued for this protected resource.');
|
|
80
|
+
}
|
|
81
|
+
if (!this.config.authorizationServers.includes(credential.issuer)) {
|
|
82
|
+
throw new Error('MCP access token issuer is not trusted by this protected resource.');
|
|
83
|
+
}
|
|
84
|
+
if (credential.expiresAtEpochSeconds <= Date.now() / 1000) {
|
|
85
|
+
throw new Error('MCP access token has expired.');
|
|
86
|
+
}
|
|
87
|
+
for (const scope of this.config.requiredScopes) {
|
|
88
|
+
if (!credential.scopes.includes(scope)) {
|
|
89
|
+
throw new Error(`MCP access token is missing required scope '${scope}'.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (credential.endpointBearerToken.trim() === '') {
|
|
93
|
+
throw new Error('MCP verifier returned an empty endpoint bearer credential.');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async call(tool, args, credential) {
|
|
97
|
+
const result = await this.dispatcher.call(tool, args ?? {}, credential.endpointBearerToken);
|
|
98
|
+
if (!result.success)
|
|
99
|
+
return this.apiError(result.error, result.requestId);
|
|
100
|
+
const outputFailure = this.registry.schemaBuilder.validate(tool.responseClass, result.value);
|
|
101
|
+
if (outputFailure) {
|
|
102
|
+
log.error(`MCP output schema violation for ${tool.apiClass.name}.${tool.methodName}: ${outputFailure.message}`);
|
|
103
|
+
return this.errorResult(new ModelVisibleToolError('implementation', 'Internal Error', result.requestId));
|
|
104
|
+
}
|
|
105
|
+
let structured;
|
|
106
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- transport boundary sanitizes serialization failures
|
|
107
|
+
try {
|
|
108
|
+
structured = this.record(result.value);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const error = (0, core_util_1.toError)(err);
|
|
112
|
+
log.error(`MCP response serialization failed for ${tool.apiClass.name}.${tool.methodName}`, error);
|
|
113
|
+
return this.errorResult(new ModelVisibleToolError('implementation', 'Internal Error', result.requestId));
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: 'text', text: JSON.stringify(structured) }],
|
|
117
|
+
structuredContent: structured,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
apiError(payload, requestId) {
|
|
121
|
+
return this.errorResult(new ModelVisibleToolError(payload.kind, payload.message, requestId, payload.field, payload.callerMessage, payload.errorCode, payload.retryAfterSeconds));
|
|
122
|
+
}
|
|
123
|
+
errorResult(error) {
|
|
124
|
+
const structured = this.record(error);
|
|
125
|
+
return {
|
|
126
|
+
content: [{ type: 'text', text: JSON.stringify(structured) }],
|
|
127
|
+
structuredContent: structured,
|
|
128
|
+
isError: true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
record(value) {
|
|
132
|
+
const json = JSON.stringify(value);
|
|
133
|
+
if (json === undefined)
|
|
134
|
+
throw new Error('MCP structured content is not JSON serializable.');
|
|
135
|
+
const parsed = JSON.parse(json);
|
|
136
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
137
|
+
// webpieces-disable no-anonymous-object-literals -- external MCP structured-content wrapper
|
|
138
|
+
return { value: parsed };
|
|
139
|
+
}
|
|
140
|
+
return parsed;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
exports.WpMcpServer = WpMcpServer;
|
|
144
|
+
//# sourceMappingURL=WpMcpServer.js.map
|
|
@@ -0,0 +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"]}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { McpAccessTokenVerifier, McpProtectedResourceMetadata, VerifiedMcpCredential, WpMcpServerConfig, } from './McpAuth';
|
|
2
|
+
export { McpToolRegistry, RegisteredMcpTool } from './McpToolRegistry';
|
|
3
|
+
export { McpApiDispatcher, McpDispatchFailure, McpDispatchSuccess, } from './McpApiDispatcher';
|
|
4
|
+
export type { McpDispatchResult } from './McpApiDispatcher';
|
|
5
|
+
export { ModelVisibleToolError, WpMcpServer } from './WpMcpServer';
|
package/src/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
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;
|
|
4
|
+
var McpAuth_1 = require("./McpAuth");
|
|
5
|
+
Object.defineProperty(exports, "McpAccessTokenVerifier", { enumerable: true, get: function () { return McpAuth_1.McpAccessTokenVerifier; } });
|
|
6
|
+
Object.defineProperty(exports, "McpProtectedResourceMetadata", { enumerable: true, get: function () { return McpAuth_1.McpProtectedResourceMetadata; } });
|
|
7
|
+
Object.defineProperty(exports, "VerifiedMcpCredential", { enumerable: true, get: function () { return McpAuth_1.VerifiedMcpCredential; } });
|
|
8
|
+
Object.defineProperty(exports, "WpMcpServerConfig", { enumerable: true, get: function () { return McpAuth_1.WpMcpServerConfig; } });
|
|
9
|
+
var McpToolRegistry_1 = require("./McpToolRegistry");
|
|
10
|
+
Object.defineProperty(exports, "McpToolRegistry", { enumerable: true, get: function () { return McpToolRegistry_1.McpToolRegistry; } });
|
|
11
|
+
Object.defineProperty(exports, "RegisteredMcpTool", { enumerable: true, get: function () { return McpToolRegistry_1.RegisteredMcpTool; } });
|
|
12
|
+
var McpApiDispatcher_1 = require("./McpApiDispatcher");
|
|
13
|
+
Object.defineProperty(exports, "McpApiDispatcher", { enumerable: true, get: function () { return McpApiDispatcher_1.McpApiDispatcher; } });
|
|
14
|
+
Object.defineProperty(exports, "McpDispatchFailure", { enumerable: true, get: function () { return McpApiDispatcher_1.McpDispatchFailure; } });
|
|
15
|
+
Object.defineProperty(exports, "McpDispatchSuccess", { enumerable: true, get: function () { return McpApiDispatcher_1.McpDispatchSuccess; } });
|
|
16
|
+
var WpMcpServer_1 = require("./WpMcpServer");
|
|
17
|
+
Object.defineProperty(exports, "ModelVisibleToolError", { enumerable: true, get: function () { return WpMcpServer_1.ModelVisibleToolError; } });
|
|
18
|
+
Object.defineProperty(exports, "WpMcpServer", { enumerable: true, get: function () { return WpMcpServer_1.WpMcpServer; } });
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
ADDED
|
@@ -0,0 +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"]}
|