@webpieces/core-util 0.4.806 → 0.4.808
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/package.json +1 -1
- package/src/http/StreamingContract.d.ts +20 -10
- package/src/http/StreamingContract.js +23 -16
- package/src/http/StreamingContract.js.map +1 -1
- package/src/http/api-type.js +2 -2
- package/src/http/api-type.js.map +1 -1
- package/src/index.d.ts +5 -4
- package/src/index.js +10 -11
- package/src/index.js.map +1 -1
- package/src/mcp/DtoSchema.d.ts +131 -81
- package/src/mcp/DtoSchema.js +239 -331
- package/src/mcp/DtoSchema.js.map +1 -1
- package/src/mcp/McpCompileAssertions.js +22 -34
- package/src/mcp/McpCompileAssertions.js.map +1 -1
- package/src/mcp/McpMetadata.d.ts +23 -17
- package/src/mcp/McpMetadata.js +25 -15
- package/src/mcp/McpMetadata.js.map +1 -1
- package/src/mcp/McpToolCatalog.d.ts +83 -0
- package/src/mcp/McpToolCatalog.js +206 -0
- package/src/mcp/McpToolCatalog.js.map +1 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpToolCatalog = exports.McpToolCatalogError = exports.McpToolDefinition = void 0;
|
|
4
|
+
const DtoSchema_1 = require("./DtoSchema");
|
|
5
|
+
const McpMetadata_1 = require("./McpMetadata");
|
|
6
|
+
/**
|
|
7
|
+
* ONE MCP tool, as `tools/list` publishes it.
|
|
8
|
+
*
|
|
9
|
+
* It is BUILT from the source by `@webpieces/api-doc-model`'s `McpSchemaRenderer` at build time, and
|
|
10
|
+
* READ here at boot — so the tool an agent is shown and the tool the server accepts are the same
|
|
11
|
+
* bytes by construction, not by two declarations agreeing.
|
|
12
|
+
*
|
|
13
|
+
* It lives in `core-util` rather than in the extractor package because both ends need it and only
|
|
14
|
+
* one end may depend on the TypeScript compiler API. `ApiJsonSchema` and {@link WpMcpToolHints},
|
|
15
|
+
* which it is made of, are already here for the same reason.
|
|
16
|
+
*/
|
|
17
|
+
class McpToolDefinition {
|
|
18
|
+
name;
|
|
19
|
+
methodName;
|
|
20
|
+
description;
|
|
21
|
+
hints;
|
|
22
|
+
inputSchema;
|
|
23
|
+
outputSchema;
|
|
24
|
+
constructor(
|
|
25
|
+
/** The stable protocol name from `@WpMcpTool('...')`. */
|
|
26
|
+
name,
|
|
27
|
+
/** The contract method it was rendered from, so a mismatch report can name the source. */
|
|
28
|
+
methodName,
|
|
29
|
+
/**
|
|
30
|
+
* The tool documentation: the method's `@mcp` tag when it has one, its JSDoc body otherwise.
|
|
31
|
+
* Documentation has exactly ONE source — the doc comment above the declaration — for the
|
|
32
|
+
* partner reading OpenAPI and the agent reading `tools/list` alike.
|
|
33
|
+
*/
|
|
34
|
+
description,
|
|
35
|
+
/**
|
|
36
|
+
* All four hints. Three are COMPUTED from `@Endpoint`'s `operation` by
|
|
37
|
+
* `mcpHintsForOperation`, which is the one place that mapping lives; `openWorldHint` comes
|
|
38
|
+
* from `@Endpoint`'s `openWorld` option.
|
|
39
|
+
*/
|
|
40
|
+
hints, inputSchema, outputSchema) {
|
|
41
|
+
this.name = name;
|
|
42
|
+
this.methodName = methodName;
|
|
43
|
+
this.description = description;
|
|
44
|
+
this.hints = hints;
|
|
45
|
+
this.inputSchema = inputSchema;
|
|
46
|
+
this.outputSchema = outputSchema;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.McpToolDefinition = McpToolDefinition;
|
|
50
|
+
/** Thrown when the catalog a server was handed is not a catalog this release can read. */
|
|
51
|
+
class McpToolCatalogError extends Error {
|
|
52
|
+
cure;
|
|
53
|
+
constructor(message,
|
|
54
|
+
/** What to do instead, in one sentence. */
|
|
55
|
+
cure) {
|
|
56
|
+
super(`${message} ${cure}`);
|
|
57
|
+
this.cure = cure;
|
|
58
|
+
this.name = 'McpToolCatalogError';
|
|
59
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.McpToolCatalogError = McpToolCatalogError;
|
|
63
|
+
/** The one cure every catalog failure prescribes: rebuild the artifact from the contracts. */
|
|
64
|
+
const REGENERATE = 'Regenerate it with `wp-openapi --manifest <manifest> --out <dir>`.';
|
|
65
|
+
/**
|
|
66
|
+
* Every MCP tool one build published, keyed by protocol name — the artifact `wp-openapi` writes as
|
|
67
|
+
* `mcp-tools.json` and `WpMcpServer` is constructed with.
|
|
68
|
+
*
|
|
69
|
+
* A tool the build never saw is a tool whose schema nobody checked, so `McpToolRegistry` fails fast
|
|
70
|
+
* at boot rather than publishing a shape derived from a second source.
|
|
71
|
+
*/
|
|
72
|
+
class McpToolCatalog {
|
|
73
|
+
tools;
|
|
74
|
+
byName;
|
|
75
|
+
constructor(tools) {
|
|
76
|
+
this.tools = tools;
|
|
77
|
+
const byName = new Map();
|
|
78
|
+
for (const tool of tools) {
|
|
79
|
+
if (byName.has(tool.name)) {
|
|
80
|
+
throw new McpToolCatalogError(`Duplicate MCP tool name '${tool.name}' in the generated catalog.`, 'Tool names are the protocol identity and must be globally unique — rename one ' +
|
|
81
|
+
"of the two @WpMcpTool('...') declarations.");
|
|
82
|
+
}
|
|
83
|
+
byName.set(tool.name, tool);
|
|
84
|
+
}
|
|
85
|
+
this.byName = byName;
|
|
86
|
+
}
|
|
87
|
+
find(name) {
|
|
88
|
+
return this.byName.get(name);
|
|
89
|
+
}
|
|
90
|
+
/** The names this catalog carries, sorted, so a failure can print what WAS published. */
|
|
91
|
+
names() {
|
|
92
|
+
return [...this.byName.keys()].sort();
|
|
93
|
+
}
|
|
94
|
+
/** The bytes `wp-openapi` writes. Sorted by name so the artifact is diffable. */
|
|
95
|
+
toJsonText() {
|
|
96
|
+
const tools = [...this.tools].sort((left, right) => left.name.localeCompare(right.name));
|
|
97
|
+
return `${JSON.stringify(tools, undefined, 2)}\n`;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Reads `mcp-tools.json`. Every nested value is rebuilt as a real class instance, because this is
|
|
101
|
+
* the one place untyped bytes become the schemas a live protocol surface publishes.
|
|
102
|
+
*
|
|
103
|
+
* Malformed JSON is not caught here: it throws `SyntaxError` to the caller's own top-level
|
|
104
|
+
* handler, which is where every other read-the-world failure in webpieces is reported.
|
|
105
|
+
*/
|
|
106
|
+
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
107
|
+
static fromJsonText(text) {
|
|
108
|
+
return new McpCatalogJson().parse(text);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
exports.McpToolCatalog = McpToolCatalog;
|
|
112
|
+
/**
|
|
113
|
+
* The PARSE BOUNDARY: untyped bytes in, real class instances out.
|
|
114
|
+
*
|
|
115
|
+
* A class of its own rather than statics on {@link McpToolCatalog} so every step is an ordinary
|
|
116
|
+
* instance method — and because it is the one place in this file where `unknown` is the correct type
|
|
117
|
+
* rather than a missing one. Nothing else in webpieces should ever hold a half-narrowed schema.
|
|
118
|
+
*/
|
|
119
|
+
class McpCatalogJson {
|
|
120
|
+
parse(text) {
|
|
121
|
+
// webpieces-disable no-any-unknown -- this IS the untyped-bytes boundary; every step below narrows it
|
|
122
|
+
const parsed = JSON.parse(text);
|
|
123
|
+
if (!Array.isArray(parsed)) {
|
|
124
|
+
throw new McpToolCatalogError('The MCP tool catalog must be a JSON array of tool definitions.', REGENERATE);
|
|
125
|
+
}
|
|
126
|
+
// webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs
|
|
127
|
+
return new McpToolCatalog(parsed.map((entry) => this.toolFrom(entry)));
|
|
128
|
+
}
|
|
129
|
+
// webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs
|
|
130
|
+
toolFrom(entry) {
|
|
131
|
+
const record = this.record(entry, 'a tool definition');
|
|
132
|
+
const hints = this.record(record['hints'], 'a tool definition hints block');
|
|
133
|
+
return new McpToolDefinition(this.text(record['name'], 'name'), this.text(record['methodName'], 'methodName'), this.text(record['description'], 'description'), new McpMetadata_1.WpMcpToolHints(hints['readOnlyHint'] === true, hints['destructiveHint'] === true, hints['idempotentHint'] === true, hints['openWorldHint'] === true), this.schemaFrom(record['inputSchema']), this.schemaFrom(record['outputSchema']));
|
|
134
|
+
}
|
|
135
|
+
// webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs
|
|
136
|
+
schemaFrom(value) {
|
|
137
|
+
const record = this.record(value, 'a JSON schema');
|
|
138
|
+
const schema = new DtoSchema_1.ApiJsonSchema(record['type']);
|
|
139
|
+
if (typeof record['description'] === 'string')
|
|
140
|
+
schema.description = record['description'];
|
|
141
|
+
if (typeof record['minimum'] === 'number')
|
|
142
|
+
schema.minimum = record['minimum'];
|
|
143
|
+
if (typeof record['maximum'] === 'number')
|
|
144
|
+
schema.maximum = record['maximum'];
|
|
145
|
+
if (Array.isArray(record['required']))
|
|
146
|
+
schema.required = record['required'];
|
|
147
|
+
if (Array.isArray(record['enum']))
|
|
148
|
+
schema.enum = record['enum'];
|
|
149
|
+
if (typeof record['x-mcp-header'] === 'string') {
|
|
150
|
+
schema['x-mcp-header'] = record['x-mcp-header'];
|
|
151
|
+
}
|
|
152
|
+
if (record['items'] !== undefined)
|
|
153
|
+
schema.items = this.schemaFrom(record['items']);
|
|
154
|
+
const extra = record['additionalProperties'];
|
|
155
|
+
if (typeof extra === 'boolean')
|
|
156
|
+
schema.additionalProperties = extra;
|
|
157
|
+
else if (extra !== undefined)
|
|
158
|
+
schema.additionalProperties = this.schemaFrom(extra);
|
|
159
|
+
const properties = record['properties'];
|
|
160
|
+
if (properties !== undefined) {
|
|
161
|
+
const built = {};
|
|
162
|
+
for (const [key, child] of Object.entries(this.record(properties, 'a schema properties block'))) {
|
|
163
|
+
built[key] = this.schemaFrom(child);
|
|
164
|
+
}
|
|
165
|
+
schema.properties = built;
|
|
166
|
+
}
|
|
167
|
+
const oneOf = record['oneOf'];
|
|
168
|
+
if (Array.isArray(oneOf)) {
|
|
169
|
+
// webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs
|
|
170
|
+
schema.oneOf = oneOf.map((branch) => this.schemaFrom(branch));
|
|
171
|
+
schema.discriminator = this.discriminatorFrom(record['discriminator']);
|
|
172
|
+
}
|
|
173
|
+
return schema;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The `discriminator` block, or undefined — a union TypeScript cannot narrow is published WITHOUT
|
|
177
|
+
* one, so an absent block is a legal document and not a missing field.
|
|
178
|
+
*/
|
|
179
|
+
// webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs
|
|
180
|
+
discriminatorFrom(value) {
|
|
181
|
+
if (value === undefined)
|
|
182
|
+
return undefined;
|
|
183
|
+
const record = this.record(value, 'a schema discriminator block');
|
|
184
|
+
const mapping = {};
|
|
185
|
+
for (const [key, branch] of Object.entries(this.record(record['mapping'], 'a discriminator mapping block'))) {
|
|
186
|
+
mapping[key] = this.text(branch, `discriminator mapping '${key}'`);
|
|
187
|
+
}
|
|
188
|
+
return new DtoSchema_1.ApiJsonSchemaDiscriminator(this.text(record['propertyName'], 'discriminator propertyName'), mapping);
|
|
189
|
+
}
|
|
190
|
+
// webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs
|
|
191
|
+
record(value, what) {
|
|
192
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
193
|
+
throw new McpToolCatalogError(`The MCP tool catalog is missing ${what}.`, REGENERATE);
|
|
194
|
+
}
|
|
195
|
+
// webpieces-disable no-any-unknown -- the narrowed result of the check above
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
// webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs
|
|
199
|
+
text(value, field) {
|
|
200
|
+
if (typeof value !== 'string') {
|
|
201
|
+
throw new McpToolCatalogError(`The MCP tool catalog has no '${field}' on a tool definition.`, REGENERATE);
|
|
202
|
+
}
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=McpToolCatalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"McpToolCatalog.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/mcp/McpToolCatalog.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiJsonSchema, ApiJsonSchemaDiscriminator, ApiJsonSchemaType } from './DtoSchema';\nimport { WpMcpToolHints } from './McpMetadata';\n\n/**\n * ONE MCP tool, as `tools/list` publishes it.\n *\n * It is BUILT from the source by `@webpieces/api-doc-model`'s `McpSchemaRenderer` at build time, and\n * READ here at boot — so the tool an agent is shown and the tool the server accepts are the same\n * bytes by construction, not by two declarations agreeing.\n *\n * It lives in `core-util` rather than in the extractor package because both ends need it and only\n * one end may depend on the TypeScript compiler API. `ApiJsonSchema` and {@link WpMcpToolHints},\n * which it is made of, are already here for the same reason.\n */\nexport class McpToolDefinition {\n constructor(\n /** The stable protocol name from `@WpMcpTool('...')`. */\n readonly name: string,\n /** The contract method it was rendered from, so a mismatch report can name the source. */\n readonly methodName: string,\n /**\n * The tool documentation: the method's `@mcp` tag when it has one, its JSDoc body otherwise.\n * Documentation has exactly ONE source — the doc comment above the declaration — for the\n * partner reading OpenAPI and the agent reading `tools/list` alike.\n */\n readonly description: string,\n /**\n * All four hints. Three are COMPUTED from `@Endpoint`'s `operation` by\n * `mcpHintsForOperation`, which is the one place that mapping lives; `openWorldHint` comes\n * from `@Endpoint`'s `openWorld` option.\n */\n readonly hints: WpMcpToolHints,\n readonly inputSchema: ApiJsonSchema,\n readonly outputSchema: ApiJsonSchema,\n ) {}\n}\n\n/** Thrown when the catalog a server was handed is not a catalog this release can read. */\nexport class McpToolCatalogError extends Error {\n constructor(\n message: string,\n /** What to do instead, in one sentence. */\n readonly cure: string,\n ) {\n super(`${message} ${cure}`);\n this.name = 'McpToolCatalogError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** The one cure every catalog failure prescribes: rebuild the artifact from the contracts. */\nconst REGENERATE = 'Regenerate it with `wp-openapi --manifest <manifest> --out <dir>`.';\n\n/**\n * Every MCP tool one build published, keyed by protocol name — the artifact `wp-openapi` writes as\n * `mcp-tools.json` and `WpMcpServer` is constructed with.\n *\n * A tool the build never saw is a tool whose schema nobody checked, so `McpToolRegistry` fails fast\n * at boot rather than publishing a shape derived from a second source.\n */\nexport class McpToolCatalog {\n private readonly byName: ReadonlyMap<string, McpToolDefinition>;\n\n constructor(readonly tools: readonly McpToolDefinition[]) {\n const byName = new Map<string, McpToolDefinition>();\n for (const tool of tools) {\n if (byName.has(tool.name)) {\n throw new McpToolCatalogError(\n `Duplicate MCP tool name '${tool.name}' in the generated catalog.`,\n 'Tool names are the protocol identity and must be globally unique — rename one ' +\n \"of the two @WpMcpTool('...') declarations.\",\n );\n }\n byName.set(tool.name, tool);\n }\n this.byName = byName;\n }\n\n find(name: string): McpToolDefinition | undefined {\n return this.byName.get(name);\n }\n\n /** The names this catalog carries, sorted, so a failure can print what WAS published. */\n names(): readonly string[] {\n return [...this.byName.keys()].sort();\n }\n\n /** The bytes `wp-openapi` writes. Sorted by name so the artifact is diffable. */\n toJsonText(): string {\n const tools = [...this.tools].sort((left: McpToolDefinition, right: McpToolDefinition) =>\n left.name.localeCompare(right.name),\n );\n return `${JSON.stringify(tools, undefined, 2)}\\n`;\n }\n\n /**\n * Reads `mcp-tools.json`. Every nested value is rebuilt as a real class instance, because this is\n * the one place untyped bytes become the schemas a live protocol surface publishes.\n *\n * Malformed JSON is not caught here: it throws `SyntaxError` to the caller's own top-level\n * handler, which is where every other read-the-world failure in webpieces is reported.\n */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromJsonText(text: string): McpToolCatalog {\n return new McpCatalogJson().parse(text);\n }\n}\n\n/**\n * The PARSE BOUNDARY: untyped bytes in, real class instances out.\n *\n * A class of its own rather than statics on {@link McpToolCatalog} so every step is an ordinary\n * instance method — and because it is the one place in this file where `unknown` is the correct type\n * rather than a missing one. Nothing else in webpieces should ever hold a half-narrowed schema.\n */\nclass McpCatalogJson {\n parse(text: string): McpToolCatalog {\n // webpieces-disable no-any-unknown -- this IS the untyped-bytes boundary; every step below narrows it\n const parsed: unknown = JSON.parse(text);\n if (!Array.isArray(parsed)) {\n throw new McpToolCatalogError(\n 'The MCP tool catalog must be a JSON array of tool definitions.',\n REGENERATE,\n );\n }\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n return new McpToolCatalog(parsed.map((entry: unknown) => this.toolFrom(entry)));\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private toolFrom(entry: unknown): McpToolDefinition {\n const record = this.record(entry, 'a tool definition');\n const hints = this.record(record['hints'], 'a tool definition hints block');\n return new McpToolDefinition(\n this.text(record['name'], 'name'),\n this.text(record['methodName'], 'methodName'),\n this.text(record['description'], 'description'),\n new WpMcpToolHints(\n hints['readOnlyHint'] === true,\n hints['destructiveHint'] === true,\n hints['idempotentHint'] === true,\n hints['openWorldHint'] === true,\n ),\n this.schemaFrom(record['inputSchema']),\n this.schemaFrom(record['outputSchema']),\n );\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private schemaFrom(value: unknown): ApiJsonSchema {\n const record = this.record(value, 'a JSON schema');\n const schema = new ApiJsonSchema(\n record['type'] as ApiJsonSchemaType | readonly ApiJsonSchemaType[],\n );\n if (typeof record['description'] === 'string') schema.description = record['description'];\n if (typeof record['minimum'] === 'number') schema.minimum = record['minimum'];\n if (typeof record['maximum'] === 'number') schema.maximum = record['maximum'];\n if (Array.isArray(record['required'])) schema.required = record['required'] as string[];\n if (Array.isArray(record['enum'])) schema.enum = record['enum'] as string[];\n if (typeof record['x-mcp-header'] === 'string') {\n schema['x-mcp-header'] = record['x-mcp-header'];\n }\n if (record['items'] !== undefined) schema.items = this.schemaFrom(record['items']);\n const extra = record['additionalProperties'];\n if (typeof extra === 'boolean') schema.additionalProperties = extra;\n else if (extra !== undefined) schema.additionalProperties = this.schemaFrom(extra);\n const properties = record['properties'];\n if (properties !== undefined) {\n const built: Record<string, ApiJsonSchema> = {};\n for (const [key, child] of Object.entries(\n this.record(properties, 'a schema properties block'),\n )) {\n built[key] = this.schemaFrom(child);\n }\n schema.properties = built;\n }\n const oneOf = record['oneOf'];\n if (Array.isArray(oneOf)) {\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n schema.oneOf = oneOf.map((branch: unknown) => this.schemaFrom(branch));\n schema.discriminator = this.discriminatorFrom(record['discriminator']);\n }\n return schema;\n }\n\n /**\n * The `discriminator` block, or undefined — a union TypeScript cannot narrow is published WITHOUT\n * one, so an absent block is a legal document and not a missing field.\n */\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private discriminatorFrom(value: unknown): ApiJsonSchemaDiscriminator | undefined {\n if (value === undefined) return undefined;\n const record = this.record(value, 'a schema discriminator block');\n const mapping: Record<string, string> = {};\n for (const [key, branch] of Object.entries(\n this.record(record['mapping'], 'a discriminator mapping block'),\n )) {\n mapping[key] = this.text(branch, `discriminator mapping '${key}'`);\n }\n return new ApiJsonSchemaDiscriminator(\n this.text(record['propertyName'], 'discriminator propertyName'),\n mapping,\n );\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private record(value: unknown, what: string): Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new McpToolCatalogError(`The MCP tool catalog is missing ${what}.`, REGENERATE);\n }\n // webpieces-disable no-any-unknown -- the narrowed result of the check above\n return value as Record<string, unknown>;\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private text(value: unknown, field: string): string {\n if (typeof value !== 'string') {\n throw new McpToolCatalogError(\n `The MCP tool catalog has no '${field}' on a tool definition.`,\n REGENERATE,\n );\n }\n return value;\n }\n}\n"]}
|