@webpieces/mcp-server 0.4.812 → 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 +2 -1
- package/package.json +6 -6
- package/src/McpToolCatalog.js +3 -3
- package/src/McpToolCatalog.js.map +1 -1
package/README.md
CHANGED
|
@@ -66,7 +66,8 @@ server.bind(app, new McpBindOptions(path, bindings, catalogs, McpDeployment.sing
|
|
|
66
66
|
(a Docker image relinking `node_modules/@myorg/*` onto `dist/`) holds the files beside its
|
|
67
67
|
`package.json`; a workspace SOURCE directory (pnpm linking onto `libraries/...` for local dev and
|
|
68
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`
|
|
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.
|
|
70
71
|
|
|
71
72
|
`McpToolRegistry` FAILS FAST at boot, naming every directory the catalogs were read from, when a bound
|
|
72
73
|
contract has no catalog, a catalog's contract is not bound, or a tool name is declared twice — and when
|
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/McpToolCatalog.js
CHANGED
|
@@ -6,9 +6,9 @@ const fs = tslib_1.__importStar(require("fs"));
|
|
|
6
6
|
const path = tslib_1.__importStar(require("path"));
|
|
7
7
|
const core_util_1 = require("@webpieces/core-util");
|
|
8
8
|
/** How every catalog failure is fixed: build the api library, whose `openapi-generate` writes them. */
|
|
9
|
-
const BUILD_IT = '
|
|
10
|
-
'
|
|
11
|
-
'`"dependsOn": ["^
|
|
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
12
|
/**
|
|
13
13
|
* ONE contract's MCP tools as a server boots them: the generated `mcp-<ContractClass>-tools.json`, and
|
|
14
14
|
* the directory it was read from, so a boot refusal can name every directory it searched.
|
|
@@ -1 +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
|
+
{"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"]}
|