@webpieces/mcp-server 0.4.811 → 0.4.813
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 +19 -12
- package/package.json +6 -6
- package/src/McpBindOptions.d.ts +13 -9
- package/src/McpBindOptions.js +8 -6
- package/src/McpBindOptions.js.map +1 -1
- package/src/McpToolCatalog.d.ts +47 -0
- package/src/McpToolCatalog.js +155 -0
- package/src/McpToolCatalog.js.map +1 -0
- package/src/McpToolRegistry.d.ts +22 -8
- package/src/McpToolRegistry.js +105 -21
- package/src/McpToolRegistry.js.map +1 -1
- package/src/WpMcpServer.js +1 -1
- package/src/WpMcpServer.js.map +1 -1
- package/src/__tests__/McpRemoteFixtures.d.ts +5 -2
- package/src/__tests__/McpRemoteFixtures.js +18 -12
- package/src/__tests__/McpRemoteFixtures.js.map +1 -1
- package/src/__tests__/WpMcpServerTestFixtures.d.ts +6 -2
- package/src/__tests__/WpMcpServerTestFixtures.js +10 -5
- package/src/__tests__/WpMcpServerTestFixtures.js.map +1 -1
- package/src/index.d.ts +1 -0
- package/src/index.js +3 -1
- package/src/index.js.map +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @webpieces/mcp-server
|
|
2
2
|
|
|
3
3
|
Publishes explicitly annotated Webpieces RPC endpoints as MCP tools. The bridge is HANDED the input
|
|
4
|
-
and output JSON Schema — the build extracts them from the contract source and writes
|
|
5
|
-
— invokes the normal Webpieces filter/controller path, and turns exceptions into safe model-visible
|
|
4
|
+
and output JSON Schema — the build extracts them from the contract source and writes one
|
|
5
|
+
`mcp-<ContractClass>-tools.json` per contract — invokes the normal Webpieces filter/controller path, and turns exceptions into safe model-visible
|
|
6
6
|
MCP results.
|
|
7
7
|
|
|
8
8
|
`@WpMcpTool` is an opt-in and every tool must also declare `@WpMcpAuthJwt`. MCP user
|
|
@@ -51,21 +51,28 @@ Every `@Endpoint` declares one enum-backed operation, independent of its require
|
|
|
51
51
|
argument: a GET may deliberately write and a POST may be a read. A tool therefore cannot claim to be
|
|
52
52
|
read-only while its endpoint declares `WRITE` — the contradiction is unrepresentable.
|
|
53
53
|
|
|
54
|
-
## Booting: the server is constructed with the generated
|
|
54
|
+
## Booting: the server is constructed with the generated catalogs
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
writes `mcp-tools.json` beside the OpenAPI documents. Hand it to `McpBindOptions`:
|
|
56
|
+
The api library's build (its `openapi-generate` target — tag the library `generate:openapi`) writes one
|
|
57
|
+
`mcp-<ContractClass>-tools.json` per MCP contract into its build output, beside the OpenAPI documents.
|
|
58
|
+
Hand them to `McpBindOptions` with ONE call that works in every environment:
|
|
61
59
|
|
|
62
60
|
```ts
|
|
63
|
-
const
|
|
64
|
-
server.bind(app, new McpBindOptions(path, bindings,
|
|
61
|
+
const catalogs = McpToolCatalog.fromPackages(['@myorg/my-apis'], __dirname);
|
|
62
|
+
server.bind(app, new McpBindOptions(path, bindings, catalogs, McpDeployment.singleProcess()));
|
|
65
63
|
```
|
|
66
64
|
|
|
67
|
-
`
|
|
68
|
-
|
|
65
|
+
`fromPackages` resolves each package from `__dirname` the way node does. A BUILT or published package
|
|
66
|
+
(a Docker image relinking `node_modules/@myorg/*` onto `dist/`) holds the files beside its
|
|
67
|
+
`package.json`; a workspace SOURCE directory (pnpm linking onto `libraries/...` for local dev and
|
|
68
|
+
vitest) holds a `project.json`, and the files are read from the outputPath of the target its
|
|
69
|
+
`openapi-generate` dependsOn (its `build`) — which is why nx.json's targetDefaults for a server's `test`
|
|
70
|
+
(and `build`) must dependsOn `^openapi-generate`; `validate-nx-wiring` prints the exact line.
|
|
71
|
+
|
|
72
|
+
`McpToolRegistry` FAILS FAST at boot, naming every directory the catalogs were read from, when a bound
|
|
73
|
+
contract has no catalog, a catalog's contract is not bound, or a tool name is declared twice — and when
|
|
74
|
+
a registered `@WpMcpTool` is absent from its contract's catalog: a tool the build never saw is a tool
|
|
75
|
+
whose schema nobody checked. Tool arguments and structured output are
|
|
69
76
|
validated against those same schemas by `ApiJsonSchemaValidator`, so the shape an agent is shown and
|
|
70
77
|
the shape the server accepts are the same bytes rather than two derivations of one contract.
|
|
71
78
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/mcp-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.813",
|
|
4
4
|
"description": "Secure MCP tool generation from annotated Webpieces API contracts",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -24,15 +24,15 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@modelcontextprotocol/node": "2.0.0",
|
|
26
26
|
"@modelcontextprotocol/server": "2.0.0",
|
|
27
|
-
"@webpieces/core-context": "0.4.
|
|
28
|
-
"@webpieces/core-util": "0.4.
|
|
29
|
-
"@webpieces/http-routing": "0.4.
|
|
30
|
-
"@webpieces/http-server": "0.4.
|
|
27
|
+
"@webpieces/core-context": "0.4.813",
|
|
28
|
+
"@webpieces/core-util": "0.4.813",
|
|
29
|
+
"@webpieces/http-routing": "0.4.813",
|
|
30
|
+
"@webpieces/http-server": "0.4.813",
|
|
31
31
|
"express": "5.1.0",
|
|
32
32
|
"reflect-metadata": "0.2.2",
|
|
33
33
|
"tslib": "2.8.1"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@webpieces/http-client-node": "0.4.
|
|
36
|
+
"@webpieces/http-client-node": "0.4.813"
|
|
37
37
|
}
|
|
38
38
|
}
|
package/src/McpBindOptions.d.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { McpToolCatalog } from '@webpieces/core-util';
|
|
2
1
|
import { McpApiBinding } from './McpApiBinding';
|
|
3
2
|
import { McpDeployment } from './McpDeployment';
|
|
3
|
+
import { McpToolCatalog } from './McpToolCatalog';
|
|
4
4
|
export declare class McpBindOptions {
|
|
5
5
|
/** Supplied by the application/discovery config; Webpieces never assumes `/mcp`. */
|
|
6
6
|
readonly endpointPath: string;
|
|
7
7
|
readonly bindings: readonly McpApiBinding[];
|
|
8
8
|
/**
|
|
9
|
-
* The tools
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
9
|
+
* The tools the build published — ONE catalog per bound contract, each the
|
|
10
|
+
* `mcp-<ContractClass>-tools.json` `wp-openapi` wrote beside its library's OpenAPI documents,
|
|
11
|
+
* read with `McpToolCatalog.fromPackages([...], __dirname)`. REQUIRED because it is the only
|
|
12
|
+
* source of a tool's documentation and schemas; there is no reflect-metadata fallback.
|
|
13
|
+
* `McpToolRegistry` refuses to boot unless it holds exactly one catalog per bound contract.
|
|
12
14
|
*/
|
|
13
|
-
readonly
|
|
15
|
+
readonly toolCatalogs: readonly McpToolCatalog[];
|
|
14
16
|
readonly deployment: McpDeployment;
|
|
15
17
|
/** Requests without Origin remain valid for non-browser MCP clients. */
|
|
16
18
|
readonly allowedOrigins: readonly string[];
|
|
@@ -20,11 +22,13 @@ export declare class McpBindOptions {
|
|
|
20
22
|
/** Supplied by the application/discovery config; Webpieces never assumes `/mcp`. */
|
|
21
23
|
endpointPath: string, bindings: readonly McpApiBinding[],
|
|
22
24
|
/**
|
|
23
|
-
* The tools
|
|
24
|
-
* `
|
|
25
|
-
*
|
|
25
|
+
* The tools the build published — ONE catalog per bound contract, each the
|
|
26
|
+
* `mcp-<ContractClass>-tools.json` `wp-openapi` wrote beside its library's OpenAPI documents,
|
|
27
|
+
* read with `McpToolCatalog.fromPackages([...], __dirname)`. REQUIRED because it is the only
|
|
28
|
+
* source of a tool's documentation and schemas; there is no reflect-metadata fallback.
|
|
29
|
+
* `McpToolRegistry` refuses to boot unless it holds exactly one catalog per bound contract.
|
|
26
30
|
*/
|
|
27
|
-
|
|
31
|
+
toolCatalogs: readonly McpToolCatalog[], deployment: McpDeployment,
|
|
28
32
|
/** Requests without Origin remain valid for non-browser MCP clients. */
|
|
29
33
|
allowedOrigins?: readonly string[], maxSubscriptions?: number, keepAliveMs?: number);
|
|
30
34
|
}
|
package/src/McpBindOptions.js
CHANGED
|
@@ -4,7 +4,7 @@ exports.McpBindOptions = void 0;
|
|
|
4
4
|
class McpBindOptions {
|
|
5
5
|
endpointPath;
|
|
6
6
|
bindings;
|
|
7
|
-
|
|
7
|
+
toolCatalogs;
|
|
8
8
|
deployment;
|
|
9
9
|
allowedOrigins;
|
|
10
10
|
maxSubscriptions;
|
|
@@ -13,16 +13,18 @@ class McpBindOptions {
|
|
|
13
13
|
/** Supplied by the application/discovery config; Webpieces never assumes `/mcp`. */
|
|
14
14
|
endpointPath, bindings,
|
|
15
15
|
/**
|
|
16
|
-
* The tools
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
16
|
+
* The tools the build published — ONE catalog per bound contract, each the
|
|
17
|
+
* `mcp-<ContractClass>-tools.json` `wp-openapi` wrote beside its library's OpenAPI documents,
|
|
18
|
+
* read with `McpToolCatalog.fromPackages([...], __dirname)`. REQUIRED because it is the only
|
|
19
|
+
* source of a tool's documentation and schemas; there is no reflect-metadata fallback.
|
|
20
|
+
* `McpToolRegistry` refuses to boot unless it holds exactly one catalog per bound contract.
|
|
19
21
|
*/
|
|
20
|
-
|
|
22
|
+
toolCatalogs, deployment,
|
|
21
23
|
/** Requests without Origin remain valid for non-browser MCP clients. */
|
|
22
24
|
allowedOrigins = [], maxSubscriptions = 1024, keepAliveMs = 15_000) {
|
|
23
25
|
this.endpointPath = endpointPath;
|
|
24
26
|
this.bindings = bindings;
|
|
25
|
-
this.
|
|
27
|
+
this.toolCatalogs = toolCatalogs;
|
|
26
28
|
this.deployment = deployment;
|
|
27
29
|
this.allowedOrigins = allowedOrigins;
|
|
28
30
|
this.maxSubscriptions = maxSubscriptions;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpBindOptions.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpBindOptions.ts"],"names":[],"mappings":";;;AAIA,MAAa,cAAc;IAGH;IACA;
|
|
1
|
+
{"version":3,"file":"McpBindOptions.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpBindOptions.ts"],"names":[],"mappings":";;;AAIA,MAAa,cAAc;IAGH;IACA;IAQA;IACA;IAEA;IACA;IACA;IAhBpB;IACI,oFAAoF;IACpE,YAAoB,EACpB,QAAkC;IAClD;;;;;;OAMG;IACa,YAAuC,EACvC,UAAyB;IACzC,wEAAwE;IACxD,iBAAoC,EAAE,EACtC,mBAAmB,IAAI,EACvB,cAAc,MAAM;QAdpB,iBAAY,GAAZ,YAAY,CAAQ;QACpB,aAAQ,GAAR,QAAQ,CAA0B;QAQlC,iBAAY,GAAZ,YAAY,CAA2B;QACvC,eAAU,GAAV,UAAU,CAAe;QAEzB,mBAAc,GAAd,cAAc,CAAwB;QACtC,qBAAgB,GAAhB,gBAAgB,CAAO;QACvB,gBAAW,GAAX,WAAW,CAAS;QAEpC,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC9D,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;QACzF,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACvE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,IAAI,gBAAgB,IAAI,CAAC,EAAE,CAAC;YAC/D,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QACxE,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;YACnD,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;QAC1E,CAAC;IACL,CAAC;CACJ;AA/BD,wCA+BC","sourcesContent":["import { McpApiBinding } from './McpApiBinding';\nimport { McpDeployment } from './McpDeployment';\nimport { McpToolCatalog } from './McpToolCatalog';\n\nexport class McpBindOptions {\n constructor(\n /** Supplied by the application/discovery config; Webpieces never assumes `/mcp`. */\n public readonly endpointPath: string,\n public readonly bindings: readonly McpApiBinding[],\n /**\n * The tools the build published — ONE catalog per bound contract, each the\n * `mcp-<ContractClass>-tools.json` `wp-openapi` wrote beside its library's OpenAPI documents,\n * read with `McpToolCatalog.fromPackages([...], __dirname)`. REQUIRED because it is the only\n * source of a tool's documentation and schemas; there is no reflect-metadata fallback.\n * `McpToolRegistry` refuses to boot unless it holds exactly one catalog per bound contract.\n */\n public readonly toolCatalogs: readonly McpToolCatalog[],\n public readonly deployment: McpDeployment,\n /** Requests without Origin remain valid for non-browser MCP clients. */\n public readonly allowedOrigins: readonly string[] = [],\n public readonly maxSubscriptions = 1024,\n public readonly keepAliveMs = 15_000,\n ) {\n if (!endpointPath.startsWith('/') || endpointPath.includes('?')) {\n throw new Error('MCP endpointPath must be an absolute path without a query string.');\n }\n if (bindings.length === 0)\n throw new Error('MCP binding requires at least one API contract.');\n if (!Number.isInteger(maxSubscriptions) || maxSubscriptions <= 0) {\n throw new Error('MCP maxSubscriptions must be a positive integer.');\n }\n if (!Number.isFinite(keepAliveMs) || keepAliveMs < 0) {\n throw new Error('MCP keepAliveMs must be zero or a positive number.');\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { McpToolCatalogFile, McpToolDefinition } from '@webpieces/core-util';
|
|
2
|
+
/**
|
|
3
|
+
* ONE contract's MCP tools as a server boots them: the generated `mcp-<ContractClass>-tools.json`, and
|
|
4
|
+
* the directory it was read from, so a boot refusal can name every directory it searched.
|
|
5
|
+
*
|
|
6
|
+
* `WpMcpServer` is handed a LIST of these (`McpBindOptions.toolCatalogs`), and `McpToolRegistry` checks
|
|
7
|
+
* each `McpApiBinding` against the catalog of exactly its own contract (#1021).
|
|
8
|
+
*
|
|
9
|
+
* Build them with {@link McpToolCatalog.fromPackages}, the one call that works in every environment a
|
|
10
|
+
* server runs in. The constructor is for a spec that hands the server catalogs it built in memory.
|
|
11
|
+
*/
|
|
12
|
+
export declare class McpToolCatalog {
|
|
13
|
+
readonly file: McpToolCatalogFile;
|
|
14
|
+
/** Where it was read from — an absolute directory, or a label for a catalog built in memory. */
|
|
15
|
+
readonly directory: string;
|
|
16
|
+
constructor(file: McpToolCatalogFile,
|
|
17
|
+
/** Where it was read from — an absolute directory, or a label for a catalog built in memory. */
|
|
18
|
+
directory: string);
|
|
19
|
+
get contractName(): string;
|
|
20
|
+
find(name: string): McpToolDefinition | undefined;
|
|
21
|
+
names(): readonly string[];
|
|
22
|
+
/**
|
|
23
|
+
* Every MCP tool catalog the named api-library packages carry — ONE call, in every environment:
|
|
24
|
+
*
|
|
25
|
+
* ```typescript
|
|
26
|
+
* McpToolCatalog.fromPackages(['@myorg/lang-website-apis'], __dirname)
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Each package is found the way node's own resolution finds it: `node_modules/<pkg>` in
|
|
30
|
+
* `resolveFrom` and every directory above it, symlinks followed. What that directory holds decides
|
|
31
|
+
* where the files are:
|
|
32
|
+
*
|
|
33
|
+
* - `mcp-*-tools.json` beside its `package.json` → a BUILT or PUBLISHED package (a Docker image that
|
|
34
|
+
* relinks `node_modules/@myorg/*` onto `dist/libraries/**`, or an npm consumer) → read them there;
|
|
35
|
+
* - a `project.json` → a workspace SOURCE directory (pnpm links `node_modules/@myorg/x` onto
|
|
36
|
+
* `libraries/.../x` for local dev and vitest) → read the `outputPath` of the target its
|
|
37
|
+
* `openapi-generate` dependsOn, resolved against the nx workspace root (found by walking up to
|
|
38
|
+
* `nx.json`), and read the files there. That is the SAME lookup the executor used to decide where
|
|
39
|
+
* to write them (`GeneratedApiDocsLayout`).
|
|
40
|
+
*
|
|
41
|
+
* @param resolveFrom where node resolution starts — pass `__dirname` of the calling file. It is
|
|
42
|
+
* required because the right answer is the CALLER's `node_modules`: in a pnpm workspace the api
|
|
43
|
+
* library is linked into the server project's own `node_modules`, which neither the process's cwd
|
|
44
|
+
* nor this package's install location can see.
|
|
45
|
+
*/
|
|
46
|
+
static fromPackages(packages: readonly string[], resolveFrom: string): McpToolCatalog[];
|
|
47
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpToolCatalog = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
8
|
+
/** How every catalog failure is fixed: build the api library, whose `openapi-generate` writes them. */
|
|
9
|
+
const BUILD_IT = 'Generate the api library\'s documents (`nx run <api-lib>:openapi-generate`, which builds it first) — ' +
|
|
10
|
+
'or, for a spec that boots the MCP server, give nx.json\'s targetDefaults for `test` ' +
|
|
11
|
+
'`"dependsOn": ["^openapi-generate"]` so every dependent generates it first.';
|
|
12
|
+
/**
|
|
13
|
+
* ONE contract's MCP tools as a server boots them: the generated `mcp-<ContractClass>-tools.json`, and
|
|
14
|
+
* the directory it was read from, so a boot refusal can name every directory it searched.
|
|
15
|
+
*
|
|
16
|
+
* `WpMcpServer` is handed a LIST of these (`McpBindOptions.toolCatalogs`), and `McpToolRegistry` checks
|
|
17
|
+
* each `McpApiBinding` against the catalog of exactly its own contract (#1021).
|
|
18
|
+
*
|
|
19
|
+
* Build them with {@link McpToolCatalog.fromPackages}, the one call that works in every environment a
|
|
20
|
+
* server runs in. The constructor is for a spec that hands the server catalogs it built in memory.
|
|
21
|
+
*/
|
|
22
|
+
class McpToolCatalog {
|
|
23
|
+
file;
|
|
24
|
+
directory;
|
|
25
|
+
constructor(file,
|
|
26
|
+
/** Where it was read from — an absolute directory, or a label for a catalog built in memory. */
|
|
27
|
+
directory) {
|
|
28
|
+
this.file = file;
|
|
29
|
+
this.directory = directory;
|
|
30
|
+
}
|
|
31
|
+
get contractName() {
|
|
32
|
+
return this.file.contractName;
|
|
33
|
+
}
|
|
34
|
+
find(name) {
|
|
35
|
+
return this.file.find(name);
|
|
36
|
+
}
|
|
37
|
+
names() {
|
|
38
|
+
return this.file.names();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Every MCP tool catalog the named api-library packages carry — ONE call, in every environment:
|
|
42
|
+
*
|
|
43
|
+
* ```typescript
|
|
44
|
+
* McpToolCatalog.fromPackages(['@myorg/lang-website-apis'], __dirname)
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* Each package is found the way node's own resolution finds it: `node_modules/<pkg>` in
|
|
48
|
+
* `resolveFrom` and every directory above it, symlinks followed. What that directory holds decides
|
|
49
|
+
* where the files are:
|
|
50
|
+
*
|
|
51
|
+
* - `mcp-*-tools.json` beside its `package.json` → a BUILT or PUBLISHED package (a Docker image that
|
|
52
|
+
* relinks `node_modules/@myorg/*` onto `dist/libraries/**`, or an npm consumer) → read them there;
|
|
53
|
+
* - a `project.json` → a workspace SOURCE directory (pnpm links `node_modules/@myorg/x` onto
|
|
54
|
+
* `libraries/.../x` for local dev and vitest) → read the `outputPath` of the target its
|
|
55
|
+
* `openapi-generate` dependsOn, resolved against the nx workspace root (found by walking up to
|
|
56
|
+
* `nx.json`), and read the files there. That is the SAME lookup the executor used to decide where
|
|
57
|
+
* to write them (`GeneratedApiDocsLayout`).
|
|
58
|
+
*
|
|
59
|
+
* @param resolveFrom where node resolution starts — pass `__dirname` of the calling file. It is
|
|
60
|
+
* required because the right answer is the CALLER's `node_modules`: in a pnpm workspace the api
|
|
61
|
+
* library is linked into the server project's own `node_modules`, which neither the process's cwd
|
|
62
|
+
* nor this package's install location can see.
|
|
63
|
+
*/
|
|
64
|
+
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
65
|
+
static fromPackages(packages, resolveFrom) {
|
|
66
|
+
if (packages.length === 0) {
|
|
67
|
+
throw new core_util_1.McpToolCatalogError('McpToolCatalog.fromPackages was given no packages.', "Name every api library whose contracts this server binds, e.g. fromPackages(['@myorg/my-apis'], __dirname).");
|
|
68
|
+
}
|
|
69
|
+
const loader = new McpPackageCatalogs(resolveFrom);
|
|
70
|
+
return packages.flatMap((packageName) => loader.load(packageName));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.McpToolCatalog = McpToolCatalog;
|
|
74
|
+
/** The filesystem half of {@link McpToolCatalog.fromPackages}: resolve, classify, read. */
|
|
75
|
+
class McpPackageCatalogs {
|
|
76
|
+
resolveFrom;
|
|
77
|
+
constructor(resolveFrom) {
|
|
78
|
+
this.resolveFrom = resolveFrom;
|
|
79
|
+
}
|
|
80
|
+
load(packageName) {
|
|
81
|
+
const packageDir = this.resolve(packageName);
|
|
82
|
+
if (this.catalogFiles(packageDir).length > 0) {
|
|
83
|
+
return this.read(packageName, packageDir, packageDir);
|
|
84
|
+
}
|
|
85
|
+
if (fs.existsSync(path.join(packageDir, 'project.json'))) {
|
|
86
|
+
return this.read(packageName, packageDir, this.outputDirOfSource(packageName, packageDir));
|
|
87
|
+
}
|
|
88
|
+
throw new core_util_1.McpToolCatalogError(`${packageName} resolved to ${packageDir}, which holds neither an mcp-<ContractClass>-tools.json ` +
|
|
89
|
+
'(a built package) nor a project.json (a workspace source directory).', BUILD_IT);
|
|
90
|
+
}
|
|
91
|
+
/** `<dir>/node_modules/<pkg>/package.json` in `resolveFrom` and every parent — node's own walk. */
|
|
92
|
+
resolve(packageName) {
|
|
93
|
+
const searched = [];
|
|
94
|
+
let dir = path.resolve(this.resolveFrom);
|
|
95
|
+
for (;;) {
|
|
96
|
+
const candidate = path.join(dir, 'node_modules', ...packageName.split('/'));
|
|
97
|
+
searched.push(candidate);
|
|
98
|
+
if (fs.existsSync(path.join(candidate, 'package.json')))
|
|
99
|
+
return fs.realpathSync(candidate);
|
|
100
|
+
const parent = path.dirname(dir);
|
|
101
|
+
if (parent === dir)
|
|
102
|
+
break;
|
|
103
|
+
dir = parent;
|
|
104
|
+
}
|
|
105
|
+
throw new core_util_1.McpToolCatalogError(`${packageName} is not installed where the MCP server can see it. Searched:\n` +
|
|
106
|
+
searched.map((each) => ` ${each}`).join('\n'), `Add ${packageName} as a dependency of the server project, and pass the calling file's ` +
|
|
107
|
+
'__dirname as fromPackages(...)\'s resolveFrom.');
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A workspace SOURCE directory: the outputPath of the target its `openapi-generate` dependsOn,
|
|
111
|
+
* against the workspace root — never an assumed `dist/`.
|
|
112
|
+
*/
|
|
113
|
+
outputDirOfSource(packageName, packageDir) {
|
|
114
|
+
const workspaceRoot = this.workspaceRootAbove(packageName, packageDir);
|
|
115
|
+
const project = JSON.parse(fs.readFileSync(path.join(packageDir, 'project.json'), 'utf8'));
|
|
116
|
+
const projectRoot = path.relative(workspaceRoot, packageDir).split(path.sep).join('/');
|
|
117
|
+
const lookup = new core_util_1.GeneratedApiDocsLayout(projectRoot, project.name ?? projectRoot, project.targets ?? {})
|
|
118
|
+
.outputTarget();
|
|
119
|
+
if (lookup.found === undefined) {
|
|
120
|
+
throw new core_util_1.McpToolCatalogError(`${packageName} is the workspace source directory ${packageDir}, and its generated MCP tool ` +
|
|
121
|
+
`catalogs cannot be located: ${lookup.problem.problem}`, lookup.problem.cure);
|
|
122
|
+
}
|
|
123
|
+
return path.resolve(workspaceRoot, lookup.found.outputPath);
|
|
124
|
+
}
|
|
125
|
+
workspaceRootAbove(packageName, packageDir) {
|
|
126
|
+
let dir = packageDir;
|
|
127
|
+
for (;;) {
|
|
128
|
+
if (fs.existsSync(path.join(dir, 'nx.json')))
|
|
129
|
+
return dir;
|
|
130
|
+
const parent = path.dirname(dir);
|
|
131
|
+
if (parent === dir)
|
|
132
|
+
break;
|
|
133
|
+
dir = parent;
|
|
134
|
+
}
|
|
135
|
+
throw new core_util_1.McpToolCatalogError(`${packageName} resolved to ${packageDir}, which has a project.json but no nx.json above it, ` +
|
|
136
|
+
'so there is no nx workspace to resolve its build outputPath against.', 'Link the BUILT package instead (its directory holds the mcp-<ContractClass>-tools.json files).');
|
|
137
|
+
}
|
|
138
|
+
read(packageName, packageDir, dir) {
|
|
139
|
+
const files = this.catalogFiles(dir);
|
|
140
|
+
if (files.length === 0) {
|
|
141
|
+
throw new core_util_1.McpToolCatalogError(`${packageName} (${packageDir}) has no mcp-<ContractClass>-tools.json in ${dir}` +
|
|
142
|
+
(fs.existsSync(dir) ? '.' : ' — the directory does not exist, so the library was never built.'), BUILD_IT);
|
|
143
|
+
}
|
|
144
|
+
return files.map((file) => new McpToolCatalog(core_util_1.McpToolCatalogFile.fromJsonText(file, fs.readFileSync(path.join(dir, file), 'utf8')), dir));
|
|
145
|
+
}
|
|
146
|
+
catalogFiles(dir) {
|
|
147
|
+
if (!fs.existsSync(dir))
|
|
148
|
+
return [];
|
|
149
|
+
return fs
|
|
150
|
+
.readdirSync(dir)
|
|
151
|
+
.filter((file) => core_util_1.McpToolCatalogFile.contractNameOf(file) !== undefined)
|
|
152
|
+
.sort();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=McpToolCatalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"McpToolCatalog.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpToolCatalog.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport {\n GeneratedApiDocsLayout,\n LayoutTargets,\n McpToolCatalogError,\n McpToolCatalogFile,\n McpToolDefinition,\n} from '@webpieces/core-util';\n\n/** How every catalog failure is fixed: build the api library, whose `openapi-generate` writes them. */\nconst BUILD_IT =\n 'Generate the api library\\'s documents (`nx run <api-lib>:openapi-generate`, which builds it first) — ' +\n 'or, for a spec that boots the MCP server, give nx.json\\'s targetDefaults for `test` ' +\n '`\"dependsOn\": [\"^openapi-generate\"]` so every dependent generates it first.';\n\n/** The two fields of an nx `project.json` this loader reads. */\ntype ProjectJson = { readonly name?: string; readonly targets?: LayoutTargets };\n\n/**\n * ONE contract's MCP tools as a server boots them: the generated `mcp-<ContractClass>-tools.json`, and\n * the directory it was read from, so a boot refusal can name every directory it searched.\n *\n * `WpMcpServer` is handed a LIST of these (`McpBindOptions.toolCatalogs`), and `McpToolRegistry` checks\n * each `McpApiBinding` against the catalog of exactly its own contract (#1021).\n *\n * Build them with {@link McpToolCatalog.fromPackages}, the one call that works in every environment a\n * server runs in. The constructor is for a spec that hands the server catalogs it built in memory.\n */\nexport class McpToolCatalog {\n constructor(\n readonly file: McpToolCatalogFile,\n /** Where it was read from — an absolute directory, or a label for a catalog built in memory. */\n readonly directory: string,\n ) {}\n\n get contractName(): string {\n return this.file.contractName;\n }\n\n find(name: string): McpToolDefinition | undefined {\n return this.file.find(name);\n }\n\n names(): readonly string[] {\n return this.file.names();\n }\n\n /**\n * Every MCP tool catalog the named api-library packages carry — ONE call, in every environment:\n *\n * ```typescript\n * McpToolCatalog.fromPackages(['@myorg/lang-website-apis'], __dirname)\n * ```\n *\n * Each package is found the way node's own resolution finds it: `node_modules/<pkg>` in\n * `resolveFrom` and every directory above it, symlinks followed. What that directory holds decides\n * where the files are:\n *\n * - `mcp-*-tools.json` beside its `package.json` → a BUILT or PUBLISHED package (a Docker image that\n * relinks `node_modules/@myorg/*` onto `dist/libraries/**`, or an npm consumer) → read them there;\n * - a `project.json` → a workspace SOURCE directory (pnpm links `node_modules/@myorg/x` onto\n * `libraries/.../x` for local dev and vitest) → read the `outputPath` of the target its\n * `openapi-generate` dependsOn, resolved against the nx workspace root (found by walking up to\n * `nx.json`), and read the files there. That is the SAME lookup the executor used to decide where\n * to write them (`GeneratedApiDocsLayout`).\n *\n * @param resolveFrom where node resolution starts — pass `__dirname` of the calling file. It is\n * required because the right answer is the CALLER's `node_modules`: in a pnpm workspace the api\n * library is linked into the server project's own `node_modules`, which neither the process's cwd\n * nor this package's install location can see.\n */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromPackages(packages: readonly string[], resolveFrom: string): McpToolCatalog[] {\n if (packages.length === 0) {\n throw new McpToolCatalogError(\n 'McpToolCatalog.fromPackages was given no packages.',\n \"Name every api library whose contracts this server binds, e.g. fromPackages(['@myorg/my-apis'], __dirname).\",\n );\n }\n const loader = new McpPackageCatalogs(resolveFrom);\n return packages.flatMap((packageName: string) => loader.load(packageName));\n }\n}\n\n/** The filesystem half of {@link McpToolCatalog.fromPackages}: resolve, classify, read. */\nclass McpPackageCatalogs {\n constructor(private readonly resolveFrom: string) {}\n\n load(packageName: string): McpToolCatalog[] {\n const packageDir = this.resolve(packageName);\n if (this.catalogFiles(packageDir).length > 0) {\n return this.read(packageName, packageDir, packageDir);\n }\n if (fs.existsSync(path.join(packageDir, 'project.json'))) {\n return this.read(packageName, packageDir, this.outputDirOfSource(packageName, packageDir));\n }\n throw new McpToolCatalogError(\n `${packageName} resolved to ${packageDir}, which holds neither an mcp-<ContractClass>-tools.json ` +\n '(a built package) nor a project.json (a workspace source directory).',\n BUILD_IT,\n );\n }\n\n /** `<dir>/node_modules/<pkg>/package.json` in `resolveFrom` and every parent — node's own walk. */\n private resolve(packageName: string): string {\n const searched: string[] = [];\n let dir = path.resolve(this.resolveFrom);\n for (;;) {\n const candidate = path.join(dir, 'node_modules', ...packageName.split('/'));\n searched.push(candidate);\n if (fs.existsSync(path.join(candidate, 'package.json'))) return fs.realpathSync(candidate);\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n throw new McpToolCatalogError(\n `${packageName} is not installed where the MCP server can see it. Searched:\\n` +\n searched.map((each: string) => ` ${each}`).join('\\n'),\n `Add ${packageName} as a dependency of the server project, and pass the calling file's ` +\n '__dirname as fromPackages(...)\\'s resolveFrom.',\n );\n }\n\n /**\n * A workspace SOURCE directory: the outputPath of the target its `openapi-generate` dependsOn,\n * against the workspace root — never an assumed `dist/`.\n */\n private outputDirOfSource(packageName: string, packageDir: string): string {\n const workspaceRoot = this.workspaceRootAbove(packageName, packageDir);\n const project = JSON.parse(fs.readFileSync(path.join(packageDir, 'project.json'), 'utf8')) as ProjectJson;\n const projectRoot = path.relative(workspaceRoot, packageDir).split(path.sep).join('/');\n const lookup = new GeneratedApiDocsLayout(projectRoot, project.name ?? projectRoot, project.targets ?? {})\n .outputTarget();\n if (lookup.found === undefined) {\n throw new McpToolCatalogError(\n `${packageName} is the workspace source directory ${packageDir}, and its generated MCP tool ` +\n `catalogs cannot be located: ${lookup.problem!.problem}`,\n lookup.problem!.cure,\n );\n }\n return path.resolve(workspaceRoot, lookup.found.outputPath);\n }\n\n private workspaceRootAbove(packageName: string, packageDir: string): string {\n let dir = packageDir;\n for (;;) {\n if (fs.existsSync(path.join(dir, 'nx.json'))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n throw new McpToolCatalogError(\n `${packageName} resolved to ${packageDir}, which has a project.json but no nx.json above it, ` +\n 'so there is no nx workspace to resolve its build outputPath against.',\n 'Link the BUILT package instead (its directory holds the mcp-<ContractClass>-tools.json files).',\n );\n }\n\n private read(packageName: string, packageDir: string, dir: string): McpToolCatalog[] {\n const files = this.catalogFiles(dir);\n if (files.length === 0) {\n throw new McpToolCatalogError(\n `${packageName} (${packageDir}) has no mcp-<ContractClass>-tools.json in ${dir}` +\n (fs.existsSync(dir) ? '.' : ' — the directory does not exist, so the library was never built.'),\n BUILD_IT,\n );\n }\n return files.map(\n (file: string) =>\n new McpToolCatalog(\n McpToolCatalogFile.fromJsonText(file, fs.readFileSync(path.join(dir, file), 'utf8')),\n dir,\n ),\n );\n }\n\n private catalogFiles(dir: string): string[] {\n if (!fs.existsSync(dir)) return [];\n return fs\n .readdirSync(dir)\n .filter((file: string) => McpToolCatalogFile.contractNameOf(file) !== undefined)\n .sort();\n }\n}\n"]}
|
package/src/McpToolRegistry.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { ApiJsonSchema, AuthMeta, EndpointOperation,
|
|
1
|
+
import { ApiJsonSchema, AuthMeta, EndpointOperation, WpMcpJwtAuthMetadata, WpMcpToolHints } from '@webpieces/core-util';
|
|
2
2
|
import { ClassType } from '@webpieces/http-routing';
|
|
3
3
|
import { McpApiBinding } from './McpApiBinding';
|
|
4
|
+
import { McpToolCatalog } from './McpToolCatalog';
|
|
4
5
|
/** Fully resolved contract metadata for one MCP tool. */
|
|
5
6
|
export declare class RegisteredMcpTool {
|
|
6
7
|
readonly apiClass: ClassType;
|
|
@@ -20,7 +21,7 @@ export declare class RegisteredMcpTool {
|
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
23
|
* Fail-fast registry: the CONTRACT says which methods are tools and how they are authorized, and the
|
|
23
|
-
* build's generated catalog says what each tool's documentation and schemas are.
|
|
24
|
+
* build's generated catalog OF THAT CONTRACT says what each tool's documentation and schemas are.
|
|
24
25
|
*
|
|
25
26
|
* ## Why the schemas come from the catalog and not from here
|
|
26
27
|
*
|
|
@@ -29,17 +30,30 @@ export declare class RegisteredMcpTool {
|
|
|
29
30
|
* contract, free to disagree; and it could not see a field's declared TYPE at all, so every fact
|
|
30
31
|
* about a field had to be restated in a `@WpDtoField` argument. #983 measured the compiler against
|
|
31
32
|
* it and found the compiler reproduces all of it, so #984 deleted the decorator and this registry
|
|
32
|
-
* now READS the same
|
|
33
|
+
* now READS the same catalogs the documents were rendered beside.
|
|
33
34
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
35
|
+
* ## One catalog per bound contract, exactly (#1021)
|
|
36
|
+
*
|
|
37
|
+
* `wp-openapi` writes one `mcp-<ContractClass>-tools.json` per contract, and this registry pairs every
|
|
38
|
+
* `McpApiBinding` with the file of its own contract. Three mismatches refuse the boot, all reported at
|
|
39
|
+
* once, each naming every directory the catalogs were read from:
|
|
40
|
+
*
|
|
41
|
+
* - a bound contract with no catalog — its tools were never given a schema by the build;
|
|
42
|
+
* - a catalog whose contract is not bound — the server was handed tools it does not serve;
|
|
43
|
+
* - a tool name declared twice — the protocol namespace is flat.
|
|
44
|
+
*
|
|
45
|
+
* A registered `@WpMcpTool` the catalog does not name is likewise a HARD FAILURE, not a fallback: a
|
|
46
|
+
* tool the build never saw is a tool whose schema nobody checked.
|
|
36
47
|
*/
|
|
37
48
|
export declare class McpToolRegistry {
|
|
38
|
-
private readonly catalog;
|
|
39
49
|
readonly tools: readonly RegisteredMcpTool[];
|
|
40
|
-
constructor(bindings: readonly McpApiBinding[],
|
|
50
|
+
constructor(bindings: readonly McpApiBinding[], catalogs: readonly McpToolCatalog[]);
|
|
41
51
|
find(name: string): RegisteredMcpTool | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* The contract checks THROW at once — they are about the contract source, not the build. A tool
|
|
54
|
+
* missing from its catalog is RECORDED on `pairing`, so one boot names every catalog mismatch.
|
|
55
|
+
*/
|
|
42
56
|
private resolve;
|
|
43
|
-
/** The generated entry for this tool, or a refusal naming what the build DID publish. */
|
|
57
|
+
/** The generated entry for this tool, or a recorded refusal naming what the build DID publish. */
|
|
44
58
|
private published;
|
|
45
59
|
}
|