@webpieces/mcp-server 0.4.811 → 0.4.812

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
@@ -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 `mcp-tools.json`
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,27 @@ 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 catalog
54
+ ## Booting: the server is constructed with the generated catalogs
55
55
 
56
- ```bash
57
- wp-openapi --manifest openapi.manifest.json --out dist
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 catalog = McpToolCatalog.fromJsonText(fs.readFileSync('dist/mcp-tools.json', 'utf8'));
64
- server.bind(app, new McpBindOptions(path, bindings, catalog, McpDeployment.singleProcess()));
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
- `McpToolRegistry` FAILS FAST at boot when a registered `@WpMcpTool` is absent from the catalog: a tool
68
- the build never saw is a tool whose schema nobody checked. Tool arguments and structured output are
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 — which is why a server's `test` target must dependsOn `^build`.
70
+
71
+ `McpToolRegistry` FAILS FAST at boot, naming every directory the catalogs were read from, when a bound
72
+ contract has no catalog, a catalog's contract is not bound, or a tool name is declared twice — and when
73
+ a registered `@WpMcpTool` is absent from its contract's catalog: a tool the build never saw is a tool
74
+ whose schema nobody checked. Tool arguments and structured output are
69
75
  validated against those same schemas by `ApiJsonSchemaValidator`, so the shape an agent is shown and
70
76
  the shape the server accepts are the same bytes rather than two derivations of one contract.
71
77
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/mcp-server",
3
- "version": "0.4.811",
3
+ "version": "0.4.812",
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.811",
28
- "@webpieces/core-util": "0.4.811",
29
- "@webpieces/http-routing": "0.4.811",
30
- "@webpieces/http-server": "0.4.811",
27
+ "@webpieces/core-context": "0.4.812",
28
+ "@webpieces/core-util": "0.4.812",
29
+ "@webpieces/http-routing": "0.4.812",
30
+ "@webpieces/http-server": "0.4.812",
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.811"
36
+ "@webpieces/http-client-node": "0.4.812"
37
37
  }
38
38
  }
@@ -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 this build published — `mcp-tools.json`, written by `wp-openapi` and read with
10
- * `McpToolCatalog.fromJsonText`. It is REQUIRED because it is the only source of a tool's
11
- * documentation and schemas; there is no reflect-metadata fallback to fall back to.
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 toolCatalog: McpToolCatalog;
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 this build published — `mcp-tools.json`, written by `wp-openapi` and read with
24
- * `McpToolCatalog.fromJsonText`. It is REQUIRED because it is the only source of a tool's
25
- * documentation and schemas; there is no reflect-metadata fallback to fall back to.
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
- toolCatalog: McpToolCatalog, deployment: McpDeployment,
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
  }
@@ -4,7 +4,7 @@ exports.McpBindOptions = void 0;
4
4
  class McpBindOptions {
5
5
  endpointPath;
6
6
  bindings;
7
- toolCatalog;
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 this build published — `mcp-tools.json`, written by `wp-openapi` and read with
17
- * `McpToolCatalog.fromJsonText`. It is REQUIRED because it is the only source of a tool's
18
- * documentation and schemas; there is no reflect-metadata fallback to fall back to.
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
- toolCatalog, deployment,
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.toolCatalog = toolCatalog;
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;IAMA;IACA;IAEA;IACA;IACA;IAdpB;IACI,oFAAoF;IACpE,YAAoB,EACpB,QAAkC;IAClD;;;;OAIG;IACa,WAA2B,EAC3B,UAAyB;IACzC,wEAAwE;IACxD,iBAAoC,EAAE,EACtC,mBAAmB,IAAI,EACvB,cAAc,MAAM;QAZpB,iBAAY,GAAZ,YAAY,CAAQ;QACpB,aAAQ,GAAR,QAAQ,CAA0B;QAMlC,gBAAW,GAAX,WAAW,CAAgB;QAC3B,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;AA7BD,wCA6BC","sourcesContent":["import { McpToolCatalog } from '@webpieces/core-util';\nimport { McpApiBinding } from './McpApiBinding';\nimport { McpDeployment } from './McpDeployment';\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 this build published — `mcp-tools.json`, written by `wp-openapi` and read with\n * `McpToolCatalog.fromJsonText`. It is REQUIRED because it is the only source of a tool's\n * documentation and schemas; there is no reflect-metadata fallback to fall back to.\n */\n public readonly toolCatalog: 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"]}
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 = 'Build the api library that declares the contracts (`nx run <api-lib>:build`, which runs its ' +
10
+ '`openapi-generate` target) — or, for a spec that boots the MCP server, give its `test` target ' +
11
+ '`"dependsOn": ["^build"]` so the library is built 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 'Build the api library that declares the contracts (`nx run <api-lib>:build`, which runs its ' +\n '`openapi-generate` target) — or, for a spec that boots the MCP server, give its `test` target ' +\n '`\"dependsOn\": [\"^build\"]` so the library is built 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"]}
@@ -1,6 +1,7 @@
1
- import { ApiJsonSchema, AuthMeta, EndpointOperation, McpToolCatalog, WpMcpJwtAuthMetadata, WpMcpToolHints } from '@webpieces/core-util';
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 `mcp-tools.json` the documents were rendered from.
33
+ * now READS the same catalogs the documents were rendered beside.
33
34
  *
34
- * A registered `@WpMcpTool` the catalog does not name is a HARD FAILURE, not a fallback: a tool the
35
- * build never saw is a tool whose schema nobody checked.
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[], catalog: McpToolCatalog);
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
  }