@webpieces/api-doc-model 0.4.804 → 0.4.805
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 +18 -1
- package/package.json +2 -2
- package/src/extract/ApiDocExtractor.d.ts +19 -0
- package/src/extract/ApiDocExtractor.js +37 -1
- package/src/extract/ApiDocExtractor.js.map +1 -1
- package/src/extract/JsDoc.d.ts +10 -0
- package/src/extract/JsDoc.js +18 -2
- package/src/extract/JsDoc.js.map +1 -1
- package/src/extract/TypeResolver.js +1 -1
- package/src/extract/TypeResolver.js.map +1 -1
- package/src/index.d.ts +3 -0
- package/src/index.js +7 -1
- package/src/index.js.map +1 -1
- package/src/model/ApiDocModel.d.ts +23 -1
- package/src/model/ApiDocModel.js +14 -1
- package/src/model/ApiDocModel.js.map +1 -1
- package/src/render/McpRenderError.d.ts +27 -0
- package/src/render/McpRenderError.js +36 -0
- package/src/render/McpRenderError.js.map +1 -0
- package/src/render/McpSchemaRenderer.d.ts +79 -0
- package/src/render/McpSchemaRenderer.js +235 -0
- package/src/render/McpSchemaRenderer.js.map +1 -0
- package/src/render/McpToolDefinition.d.ts +44 -0
- package/src/render/McpToolDefinition.js +44 -0
- package/src/render/McpToolDefinition.js.map +1 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpSchemaRenderer = void 0;
|
|
4
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
5
|
+
const McpRenderError_1 = require("./McpRenderError");
|
|
6
|
+
const McpToolDefinition_1 = require("./McpToolDefinition");
|
|
7
|
+
/** RFC 9110 token, which is what an `Mcp-Param-{name}` header name has to be. */
|
|
8
|
+
const HEADER_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
9
|
+
/**
|
|
10
|
+
* `ApiDocModel` -> the MCP tool list, in exactly the shape `DtoSchemaBuilder` produces at boot from
|
|
11
|
+
* reflect-metadata.
|
|
12
|
+
*
|
|
13
|
+
* ## Why this exists at all, and why it is READ-ONLY
|
|
14
|
+
*
|
|
15
|
+
* #984 wants the MCP runtime off reflect-metadata, which deletes every erasure-repair argument of
|
|
16
|
+
* `@WpDtoField` — `required`, `arrayItems`, `integer`, `minimum`, `maximum`, `enumValues`. That is a
|
|
17
|
+
* behaviour change on a live protocol surface, and its failure mode is silent: a tool whose input
|
|
18
|
+
* schema quietly loses a `required` entry or an `enum` starts failing agent calls at RUNTIME, not at
|
|
19
|
+
* build. This renderer plus the equivalence spec beside it turn "the compiler can obviously replace
|
|
20
|
+
* those arguments" from a plausible argument into a MEASURED one (#983).
|
|
21
|
+
*
|
|
22
|
+
* ## It deliberately reproduces `DtoSchemaBuilder`, defect-for-defect
|
|
23
|
+
*
|
|
24
|
+
* Where MCP's `ApiJsonSchema` subset could express MORE than the runtime does, this renderer emits
|
|
25
|
+
* what the RUNTIME emits, and the gate's report names the difference. Three concrete cases:
|
|
26
|
+
*
|
|
27
|
+
* - **NULLABLE is not rendered.** `ApiJsonSchema.type` holds one string, so `type: [T, "null"]` is
|
|
28
|
+
* not expressible in the type the runtime publishes. The compiler can SEE `externalId: string | null`
|
|
29
|
+
* and the runtime cannot; emitting it here would fail the gate on a difference that is a runtime
|
|
30
|
+
* capability GAP rather than an extractor bug, and hide the real mismatches under it.
|
|
31
|
+
* - **A nested DTO is INLINED**, with the FIELD's prose on it, because `buildAt` inlines and then
|
|
32
|
+
* `fieldSchema` overwrites `description`. MCP has no `$ref`.
|
|
33
|
+
* - **A bound on an ARRAY of numbers** is put on the ITEM, where OpenAPI puts it; the runtime cannot
|
|
34
|
+
* express it at all (`@WpDtoField` rejects numeric constraints on a non-`Number` field).
|
|
35
|
+
*
|
|
36
|
+
* Making the comparison agree by WEAKENING it would destroy the only thing the gate is for, so every
|
|
37
|
+
* one of those is a documented, deliberate reproduction rather than a relaxation.
|
|
38
|
+
*/
|
|
39
|
+
class McpSchemaRenderer {
|
|
40
|
+
model;
|
|
41
|
+
constructor(model) {
|
|
42
|
+
this.model = model;
|
|
43
|
+
}
|
|
44
|
+
/** Every `@WpMcpTool` method of the contract, in declaration order. */
|
|
45
|
+
render() {
|
|
46
|
+
const tools = [];
|
|
47
|
+
for (const endpoint of this.model.endpoints) {
|
|
48
|
+
if (endpoint.mcpTool !== undefined) {
|
|
49
|
+
tools.push(this.tool(endpoint));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return tools;
|
|
53
|
+
}
|
|
54
|
+
tool(endpoint) {
|
|
55
|
+
const where = `${this.model.contractName}.${endpoint.methodName}`;
|
|
56
|
+
const description = endpoint.mcpDescription ?? endpoint.description;
|
|
57
|
+
if (description.trim() === '') {
|
|
58
|
+
throw new McpRenderError_1.McpRenderError('an MCP tool has no documentation', where, 'Write a JSDoc block on the method, or an @mcp tag for agent-facing wording. It is ' +
|
|
59
|
+
'published verbatim by tools/list, so an agent has nothing else to go on.');
|
|
60
|
+
}
|
|
61
|
+
if (endpoint.request === undefined || endpoint.response === undefined) {
|
|
62
|
+
throw new McpRenderError_1.McpRenderError('an MCP tool has no declared request or response type', where, 'Declare both: one request parameter and a Promise<Response> return type.');
|
|
63
|
+
}
|
|
64
|
+
return new McpToolDefinition_1.McpToolDefinition(endpoint.mcpTool.name, endpoint.methodName, description, (0, core_util_1.mcpHintsForOperation)(McpSchemaRenderer.operationOf(endpoint, where), endpoint.openWorld), this.rootSchema(endpoint.request, where, 'request'), this.rootSchema(endpoint.response, where, 'response'));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* `operation` verbatim from the model, mapped back onto the REAL constant.
|
|
68
|
+
*
|
|
69
|
+
* A `switch` and not a cast, because the model carries the operation as a string and a cast would
|
|
70
|
+
* hand `mcpHintsForOperation` a value it has no case for — which, for a function whose three arms
|
|
71
|
+
* are exhaustive, means falling off the end and publishing a tool with NO hints.
|
|
72
|
+
*/
|
|
73
|
+
// webpieces-disable no-function-outside-class -- private static mapping of this class
|
|
74
|
+
static operationOf(endpoint, where) {
|
|
75
|
+
switch (endpoint.operation) {
|
|
76
|
+
case core_util_1.READ:
|
|
77
|
+
return core_util_1.READ;
|
|
78
|
+
case core_util_1.WRITE_IDEMPOTENT:
|
|
79
|
+
return core_util_1.WRITE_IDEMPOTENT;
|
|
80
|
+
case core_util_1.WRITE:
|
|
81
|
+
return core_util_1.WRITE;
|
|
82
|
+
default:
|
|
83
|
+
throw new McpRenderError_1.McpRenderError(`@Endpoint declares operation '${endpoint.operation}', which has no MCP hints`, where, `Use one of the exported constants: ${core_util_1.READ}, ${core_util_1.WRITE_IDEMPOTENT}, ${core_util_1.WRITE}.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** A tool's input/output schema: always a closed object, exactly as `DtoSchemaBuilder.build` is. */
|
|
87
|
+
rootSchema(ref, where, side) {
|
|
88
|
+
if (ref.kind !== 'ref') {
|
|
89
|
+
throw new McpRenderError_1.McpRenderError(`an MCP tool's ${side} is not a named DTO`, where, 'Give it a named interface or class. An inline or primitive ' +
|
|
90
|
+
`${side} has no object schema, and MCP publishes objects.`);
|
|
91
|
+
}
|
|
92
|
+
return this.objectSchema(this.namedType(ref.refName, where), new Set());
|
|
93
|
+
}
|
|
94
|
+
namedType(name, where) {
|
|
95
|
+
const type = this.model.types.get(name);
|
|
96
|
+
if (type === undefined) {
|
|
97
|
+
throw new McpRenderError_1.McpRenderError(`no model entry for type '${name}'`, where, 'Declare the type in a file the extractor reaches from this contract.');
|
|
98
|
+
}
|
|
99
|
+
return type;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* One object DTO. CLOSED (`additionalProperties: false`), `required` only when non-empty — both
|
|
103
|
+
* exactly as `DtoSchemaBuilder.buildAt` writes them, because that is what is being compared.
|
|
104
|
+
*
|
|
105
|
+
* The cycle stop is `parents`, and it THROWS rather than truncating, which is again what the
|
|
106
|
+
* runtime does: an inline schema cannot express a recursive DTO, and one that silently stopped a
|
|
107
|
+
* level down would publish a shape the server does not accept.
|
|
108
|
+
*/
|
|
109
|
+
objectSchema(type, parents) {
|
|
110
|
+
if (parents.has(type.name)) {
|
|
111
|
+
throw new McpRenderError_1.McpRenderError(`recursive DTO '${type.name}' cannot use an inline MCP schema`, type.name, 'Break the cycle, or keep this DTO out of the MCP document — a tool schema is ' +
|
|
112
|
+
'inline and has no $ref to close a loop with.');
|
|
113
|
+
}
|
|
114
|
+
if (type.unionRefNames.length > 0) {
|
|
115
|
+
throw new McpRenderError_1.McpRenderError(`'${type.name}' is a union, which an MCP input schema cannot express`, type.name, 'Publish a single object shape to agents, or keep this contract off MCP.');
|
|
116
|
+
}
|
|
117
|
+
if (type.indexSignatureValue !== undefined && type.fields.length > 0) {
|
|
118
|
+
throw new McpRenderError_1.McpRenderError(`'${type.name}' has both named fields and an index signature`, type.name, 'Split the open map into a field of its own: a schema is either closed or a typed ' +
|
|
119
|
+
'map, and MCP has no spelling for half of each.');
|
|
120
|
+
}
|
|
121
|
+
const nested = new Set(parents);
|
|
122
|
+
nested.add(type.name);
|
|
123
|
+
const schema = new core_util_1.ApiJsonSchema('object');
|
|
124
|
+
schema.properties = {};
|
|
125
|
+
schema.additionalProperties = false;
|
|
126
|
+
const required = [];
|
|
127
|
+
for (const field of type.fields) {
|
|
128
|
+
schema.properties[field.name] = this.fieldSchema(type, field, nested);
|
|
129
|
+
if (!field.optional) {
|
|
130
|
+
required.push(field.name);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (required.length > 0) {
|
|
134
|
+
schema.required = required;
|
|
135
|
+
}
|
|
136
|
+
return schema;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* One FIELD: its type, then the prose and the constraints that hang off the field.
|
|
140
|
+
*
|
|
141
|
+
* The ORDER matters and mirrors `fieldSchema`: the type is built first and `description` is
|
|
142
|
+
* written over whatever the type produced, so a nested DTO carries the FIELD's sentence rather
|
|
143
|
+
* than the DTO's. `@WpMin` / `@WpMax` land on the numeric LEAF — on an array, on the item — for
|
|
144
|
+
* the same reason `SchemaRenderer` puts them there: a `minimum` on an array means nothing.
|
|
145
|
+
*/
|
|
146
|
+
fieldSchema(owner, field, parents) {
|
|
147
|
+
const where = `${owner.name}.${field.name}`;
|
|
148
|
+
const schema = this.typeSchema(field.type, where, parents);
|
|
149
|
+
const description = field.mcpDescription ?? field.description;
|
|
150
|
+
if (description.trim() === '') {
|
|
151
|
+
throw new McpRenderError_1.McpRenderError('a published DTO field has no documentation', where, 'Write a JSDoc sentence on the field. It is the only thing an agent is told about ' +
|
|
152
|
+
'that parameter.');
|
|
153
|
+
}
|
|
154
|
+
schema.description = description;
|
|
155
|
+
const leaf = field.type.kind === 'array' ? schema.items : schema;
|
|
156
|
+
if (field.min !== undefined) {
|
|
157
|
+
leaf.minimum = field.min;
|
|
158
|
+
}
|
|
159
|
+
if (field.max !== undefined) {
|
|
160
|
+
leaf.maximum = field.max;
|
|
161
|
+
}
|
|
162
|
+
if (field.mcpHeader !== undefined) {
|
|
163
|
+
McpSchemaRenderer.assertHeaderFits(field, schema, where);
|
|
164
|
+
schema['x-mcp-header'] = field.mcpHeader;
|
|
165
|
+
}
|
|
166
|
+
return schema;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* `@mcpHeader` mirrors a PRIMITIVE parameter into `Mcp-Param-{name}` (MCP 2026 SEP-2243). The
|
|
170
|
+
* three conditions are the runtime's, restated against the compiler's view of the field, so a
|
|
171
|
+
* declaration the server would reject at boot fails the document build instead.
|
|
172
|
+
*/
|
|
173
|
+
// webpieces-disable no-function-outside-class -- private static validator of this class
|
|
174
|
+
static assertHeaderFits(field, schema, where) {
|
|
175
|
+
if (!HEADER_TOKEN.test(field.mcpHeader)) {
|
|
176
|
+
throw new McpRenderError_1.McpRenderError(`@mcpHeader '${field.mcpHeader}' is not an RFC 9110 token`, where, 'Use letters, digits and the token punctuation only — it becomes an HTTP header name.');
|
|
177
|
+
}
|
|
178
|
+
if (schema.type !== 'string' && schema.type !== 'boolean' && schema.type !== 'integer') {
|
|
179
|
+
throw new McpRenderError_1.McpRenderError('@mcpHeader is on a field that is not a primitive an MCP header can carry', where, 'Mirror a string, a boolean or an Integer. A non-integer number and every object ' +
|
|
180
|
+
'shape are excluded by MCP 2026-07-28.');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** One resolved type, with no field-level prose or constraints on it. */
|
|
184
|
+
typeSchema(ref, where, parents) {
|
|
185
|
+
switch (ref.kind) {
|
|
186
|
+
case 'primitive':
|
|
187
|
+
return McpSchemaRenderer.primitive(ref, where);
|
|
188
|
+
case 'enum': {
|
|
189
|
+
const schema = new core_util_1.ApiJsonSchema('string');
|
|
190
|
+
schema.enum = ref.enumValues.slice();
|
|
191
|
+
return schema;
|
|
192
|
+
}
|
|
193
|
+
case 'array': {
|
|
194
|
+
const schema = new core_util_1.ApiJsonSchema('array');
|
|
195
|
+
schema.items = this.typeSchema(ref.items, where, parents);
|
|
196
|
+
return schema;
|
|
197
|
+
}
|
|
198
|
+
case 'openMap': {
|
|
199
|
+
const schema = new core_util_1.ApiJsonSchema('object');
|
|
200
|
+
schema.additionalProperties = this.typeSchema(ref.values, where, parents);
|
|
201
|
+
return schema;
|
|
202
|
+
}
|
|
203
|
+
case 'ref':
|
|
204
|
+
return this.referencedSchema(ref.refName, where, parents);
|
|
205
|
+
case 'union':
|
|
206
|
+
throw new McpRenderError_1.McpRenderError('a union has no MCP input-schema shape', where, 'Publish one object shape to agents, or keep this method off MCP.');
|
|
207
|
+
default:
|
|
208
|
+
throw new McpRenderError_1.McpRenderError(`no MCP schema for the declared type '${ref.unmappedText ?? '<unknown>'}'`, where, 'Give the field a shape the model can represent — a named DTO, an array of one, ' +
|
|
209
|
+
'a string-literal union, a Record, or a primitive.');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** A named type: a string enum becomes `enum`, an object DTO is INLINED (MCP has no `$ref`). */
|
|
213
|
+
referencedSchema(name, where, parents) {
|
|
214
|
+
const type = this.namedType(name, where);
|
|
215
|
+
if (type.enumValues.length > 0) {
|
|
216
|
+
const schema = new core_util_1.ApiJsonSchema('string');
|
|
217
|
+
schema.enum = type.enumValues.slice();
|
|
218
|
+
return schema;
|
|
219
|
+
}
|
|
220
|
+
return this.objectSchema(type, parents);
|
|
221
|
+
}
|
|
222
|
+
// webpieces-disable no-function-outside-class -- private static mapping of this class
|
|
223
|
+
static primitive(ref, where) {
|
|
224
|
+
if (ref.primitive === 'string' || ref.primitive === 'boolean') {
|
|
225
|
+
return new core_util_1.ApiJsonSchema(ref.primitive);
|
|
226
|
+
}
|
|
227
|
+
if (ref.primitive === 'number') {
|
|
228
|
+
return new core_util_1.ApiJsonSchema(ref.integer ? 'integer' : 'number');
|
|
229
|
+
}
|
|
230
|
+
throw new McpRenderError_1.McpRenderError(`'${ref.primitive}' has no MCP schema`, where, 'Give the field a concrete type. `unknown`, `any`, `void` and a bare `null` publish as ' +
|
|
231
|
+
'"anything", which an agent cannot fill in.');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
exports.McpSchemaRenderer = McpSchemaRenderer;
|
|
235
|
+
//# sourceMappingURL=McpSchemaRenderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"McpSchemaRenderer.js","sourceRoot":"","sources":["../../../../../../packages/docs/api-doc-model/src/render/McpSchemaRenderer.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ApiJsonSchema,\n EndpointOperation,\n mcpHintsForOperation,\n READ,\n WRITE,\n WRITE_IDEMPOTENT,\n} from '@webpieces/core-util';\nimport {\n ApiDocModel,\n DocumentedEndpoint,\n DocumentedField,\n DocumentedType,\n} from '../model/ApiDocModel';\nimport { TypeRef } from '../model/TypeRef';\nimport { McpRenderError } from './McpRenderError';\nimport { McpToolDefinition } from './McpToolDefinition';\n\n/** RFC 9110 token, which is what an `Mcp-Param-{name}` header name has to be. */\nconst HEADER_TOKEN = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/;\n\n/**\n * `ApiDocModel` -> the MCP tool list, in exactly the shape `DtoSchemaBuilder` produces at boot from\n * reflect-metadata.\n *\n * ## Why this exists at all, and why it is READ-ONLY\n *\n * #984 wants the MCP runtime off reflect-metadata, which deletes every erasure-repair argument of\n * `@WpDtoField` — `required`, `arrayItems`, `integer`, `minimum`, `maximum`, `enumValues`. That is a\n * behaviour change on a live protocol surface, and its failure mode is silent: a tool whose input\n * schema quietly loses a `required` entry or an `enum` starts failing agent calls at RUNTIME, not at\n * build. This renderer plus the equivalence spec beside it turn \"the compiler can obviously replace\n * those arguments\" from a plausible argument into a MEASURED one (#983).\n *\n * ## It deliberately reproduces `DtoSchemaBuilder`, defect-for-defect\n *\n * Where MCP's `ApiJsonSchema` subset could express MORE than the runtime does, this renderer emits\n * what the RUNTIME emits, and the gate's report names the difference. Three concrete cases:\n *\n * - **NULLABLE is not rendered.** `ApiJsonSchema.type` holds one string, so `type: [T, \"null\"]` is\n * not expressible in the type the runtime publishes. The compiler can SEE `externalId: string | null`\n * and the runtime cannot; emitting it here would fail the gate on a difference that is a runtime\n * capability GAP rather than an extractor bug, and hide the real mismatches under it.\n * - **A nested DTO is INLINED**, with the FIELD's prose on it, because `buildAt` inlines and then\n * `fieldSchema` overwrites `description`. MCP has no `$ref`.\n * - **A bound on an ARRAY of numbers** is put on the ITEM, where OpenAPI puts it; the runtime cannot\n * express it at all (`@WpDtoField` rejects numeric constraints on a non-`Number` field).\n *\n * Making the comparison agree by WEAKENING it would destroy the only thing the gate is for, so every\n * one of those is a documented, deliberate reproduction rather than a relaxation.\n */\nexport class McpSchemaRenderer {\n constructor(private readonly model: ApiDocModel) {}\n\n /** Every `@WpMcpTool` method of the contract, in declaration order. */\n render(): readonly McpToolDefinition[] {\n const tools: McpToolDefinition[] = [];\n for (const endpoint of this.model.endpoints) {\n if (endpoint.mcpTool !== undefined) {\n tools.push(this.tool(endpoint));\n }\n }\n return tools;\n }\n\n private tool(endpoint: DocumentedEndpoint): McpToolDefinition {\n const where = `${this.model.contractName}.${endpoint.methodName}`;\n const description = endpoint.mcpDescription ?? endpoint.description;\n if (description.trim() === '') {\n throw new McpRenderError(\n 'an MCP tool has no documentation',\n where,\n 'Write a JSDoc block on the method, or an @mcp tag for agent-facing wording. It is ' +\n 'published verbatim by tools/list, so an agent has nothing else to go on.',\n );\n }\n if (endpoint.request === undefined || endpoint.response === undefined) {\n throw new McpRenderError(\n 'an MCP tool has no declared request or response type',\n where,\n 'Declare both: one request parameter and a Promise<Response> return type.',\n );\n }\n return new McpToolDefinition(\n endpoint.mcpTool!.name,\n endpoint.methodName,\n description,\n mcpHintsForOperation(\n McpSchemaRenderer.operationOf(endpoint, where),\n endpoint.openWorld,\n ),\n this.rootSchema(endpoint.request, where, 'request'),\n this.rootSchema(endpoint.response, where, 'response'),\n );\n }\n\n /**\n * `operation` verbatim from the model, mapped back onto the REAL constant.\n *\n * A `switch` and not a cast, because the model carries the operation as a string and a cast would\n * hand `mcpHintsForOperation` a value it has no case for — which, for a function whose three arms\n * are exhaustive, means falling off the end and publishing a tool with NO hints.\n */\n // webpieces-disable no-function-outside-class -- private static mapping of this class\n private static operationOf(endpoint: DocumentedEndpoint, where: string): EndpointOperation {\n switch (endpoint.operation) {\n case READ:\n return READ;\n case WRITE_IDEMPOTENT:\n return WRITE_IDEMPOTENT;\n case WRITE:\n return WRITE;\n default:\n throw new McpRenderError(\n `@Endpoint declares operation '${endpoint.operation}', which has no MCP hints`,\n where,\n `Use one of the exported constants: ${READ}, ${WRITE_IDEMPOTENT}, ${WRITE}.`,\n );\n }\n }\n\n /** A tool's input/output schema: always a closed object, exactly as `DtoSchemaBuilder.build` is. */\n private rootSchema(ref: TypeRef, where: string, side: string): ApiJsonSchema {\n if (ref.kind !== 'ref') {\n throw new McpRenderError(\n `an MCP tool's ${side} is not a named DTO`,\n where,\n 'Give it a named interface or class. An inline or primitive ' +\n `${side} has no object schema, and MCP publishes objects.`,\n );\n }\n return this.objectSchema(this.namedType(ref.refName!, where), new Set<string>());\n }\n\n private namedType(name: string, where: string): DocumentedType {\n const type = this.model.types.get(name);\n if (type === undefined) {\n throw new McpRenderError(\n `no model entry for type '${name}'`,\n where,\n 'Declare the type in a file the extractor reaches from this contract.',\n );\n }\n return type;\n }\n\n /**\n * One object DTO. CLOSED (`additionalProperties: false`), `required` only when non-empty — both\n * exactly as `DtoSchemaBuilder.buildAt` writes them, because that is what is being compared.\n *\n * The cycle stop is `parents`, and it THROWS rather than truncating, which is again what the\n * runtime does: an inline schema cannot express a recursive DTO, and one that silently stopped a\n * level down would publish a shape the server does not accept.\n */\n private objectSchema(type: DocumentedType, parents: ReadonlySet<string>): ApiJsonSchema {\n if (parents.has(type.name)) {\n throw new McpRenderError(\n `recursive DTO '${type.name}' cannot use an inline MCP schema`,\n type.name,\n 'Break the cycle, or keep this DTO out of the MCP document — a tool schema is ' +\n 'inline and has no $ref to close a loop with.',\n );\n }\n if (type.unionRefNames.length > 0) {\n throw new McpRenderError(\n `'${type.name}' is a union, which an MCP input schema cannot express`,\n type.name,\n 'Publish a single object shape to agents, or keep this contract off MCP.',\n );\n }\n if (type.indexSignatureValue !== undefined && type.fields.length > 0) {\n throw new McpRenderError(\n `'${type.name}' has both named fields and an index signature`,\n type.name,\n 'Split the open map into a field of its own: a schema is either closed or a typed ' +\n 'map, and MCP has no spelling for half of each.',\n );\n }\n const nested = new Set(parents);\n nested.add(type.name);\n\n const schema = new ApiJsonSchema('object');\n schema.properties = {};\n schema.additionalProperties = false;\n const required: string[] = [];\n for (const field of type.fields) {\n schema.properties[field.name] = this.fieldSchema(type, field, nested);\n if (!field.optional) {\n required.push(field.name);\n }\n }\n if (required.length > 0) {\n schema.required = required;\n }\n return schema;\n }\n\n /**\n * One FIELD: its type, then the prose and the constraints that hang off the field.\n *\n * The ORDER matters and mirrors `fieldSchema`: the type is built first and `description` is\n * written over whatever the type produced, so a nested DTO carries the FIELD's sentence rather\n * than the DTO's. `@WpMin` / `@WpMax` land on the numeric LEAF — on an array, on the item — for\n * the same reason `SchemaRenderer` puts them there: a `minimum` on an array means nothing.\n */\n private fieldSchema(\n owner: DocumentedType,\n field: DocumentedField,\n parents: ReadonlySet<string>,\n ): ApiJsonSchema {\n const where = `${owner.name}.${field.name}`;\n const schema = this.typeSchema(field.type, where, parents);\n const description = field.mcpDescription ?? field.description;\n if (description.trim() === '') {\n throw new McpRenderError(\n 'a published DTO field has no documentation',\n where,\n 'Write a JSDoc sentence on the field. It is the only thing an agent is told about ' +\n 'that parameter.',\n );\n }\n schema.description = description;\n\n const leaf = field.type.kind === 'array' ? schema.items! : schema;\n if (field.min !== undefined) {\n leaf.minimum = field.min;\n }\n if (field.max !== undefined) {\n leaf.maximum = field.max;\n }\n if (field.mcpHeader !== undefined) {\n McpSchemaRenderer.assertHeaderFits(field, schema, where);\n schema['x-mcp-header'] = field.mcpHeader;\n }\n return schema;\n }\n\n /**\n * `@mcpHeader` mirrors a PRIMITIVE parameter into `Mcp-Param-{name}` (MCP 2026 SEP-2243). The\n * three conditions are the runtime's, restated against the compiler's view of the field, so a\n * declaration the server would reject at boot fails the document build instead.\n */\n // webpieces-disable no-function-outside-class -- private static validator of this class\n private static assertHeaderFits(\n field: DocumentedField,\n schema: ApiJsonSchema,\n where: string,\n ): void {\n if (!HEADER_TOKEN.test(field.mcpHeader!)) {\n throw new McpRenderError(\n `@mcpHeader '${field.mcpHeader}' is not an RFC 9110 token`,\n where,\n 'Use letters, digits and the token punctuation only — it becomes an HTTP header name.',\n );\n }\n if (schema.type !== 'string' && schema.type !== 'boolean' && schema.type !== 'integer') {\n throw new McpRenderError(\n '@mcpHeader is on a field that is not a primitive an MCP header can carry',\n where,\n 'Mirror a string, a boolean or an Integer. A non-integer number and every object ' +\n 'shape are excluded by MCP 2026-07-28.',\n );\n }\n }\n\n /** One resolved type, with no field-level prose or constraints on it. */\n private typeSchema(ref: TypeRef, where: string, parents: ReadonlySet<string>): ApiJsonSchema {\n switch (ref.kind) {\n case 'primitive':\n return McpSchemaRenderer.primitive(ref, where);\n case 'enum': {\n const schema = new ApiJsonSchema('string');\n schema.enum = ref.enumValues.slice();\n return schema;\n }\n case 'array': {\n const schema = new ApiJsonSchema('array');\n schema.items = this.typeSchema(ref.items!, where, parents);\n return schema;\n }\n case 'openMap': {\n const schema = new ApiJsonSchema('object');\n schema.additionalProperties = this.typeSchema(ref.values!, where, parents);\n return schema;\n }\n case 'ref':\n return this.referencedSchema(ref.refName!, where, parents);\n case 'union':\n throw new McpRenderError(\n 'a union has no MCP input-schema shape',\n where,\n 'Publish one object shape to agents, or keep this method off MCP.',\n );\n default:\n throw new McpRenderError(\n `no MCP schema for the declared type '${ref.unmappedText ?? '<unknown>'}'`,\n where,\n 'Give the field a shape the model can represent — a named DTO, an array of one, ' +\n 'a string-literal union, a Record, or a primitive.',\n );\n }\n }\n\n /** A named type: a string enum becomes `enum`, an object DTO is INLINED (MCP has no `$ref`). */\n private referencedSchema(\n name: string,\n where: string,\n parents: ReadonlySet<string>,\n ): ApiJsonSchema {\n const type = this.namedType(name, where);\n if (type.enumValues.length > 0) {\n const schema = new ApiJsonSchema('string');\n schema.enum = type.enumValues.slice();\n return schema;\n }\n return this.objectSchema(type, parents);\n }\n\n // webpieces-disable no-function-outside-class -- private static mapping of this class\n private static primitive(ref: TypeRef, where: string): ApiJsonSchema {\n if (ref.primitive === 'string' || ref.primitive === 'boolean') {\n return new ApiJsonSchema(ref.primitive);\n }\n if (ref.primitive === 'number') {\n return new ApiJsonSchema(ref.integer ? 'integer' : 'number');\n }\n throw new McpRenderError(\n `'${ref.primitive}' has no MCP schema`,\n where,\n 'Give the field a concrete type. `unknown`, `any`, `void` and a bare `null` publish as ' +\n '\"anything\", which an agent cannot fill in.',\n );\n }\n}\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ApiJsonSchema, WpMcpToolHints } from '@webpieces/core-util';
|
|
2
|
+
/**
|
|
3
|
+
* ONE MCP tool, as `tools/list` publishes it, rendered from the `ApiDocModel`.
|
|
4
|
+
*
|
|
5
|
+
* Every field here has a RUNTIME counterpart on `RegisteredMcpTool` in `@webpieces/mcp-server`, and
|
|
6
|
+
* the equivalence gate (#983) asserts the two are equal field by field. That is the whole reason this
|
|
7
|
+
* class mirrors that one's shape rather than inventing a tidier one: a difference in SHAPE would hide
|
|
8
|
+
* a difference in CONTENT, and the content is the thing being measured.
|
|
9
|
+
*/
|
|
10
|
+
export declare class McpToolDefinition {
|
|
11
|
+
/** The stable protocol name from `@WpMcpTool({name})`. */
|
|
12
|
+
readonly name: string;
|
|
13
|
+
/** The contract method it was rendered from, so a mismatch report can name the source. */
|
|
14
|
+
readonly methodName: string;
|
|
15
|
+
/**
|
|
16
|
+
* The tool documentation: the method's `@mcp` tag when it has one, its JSDoc body otherwise.
|
|
17
|
+
* NOT `@WpMcpTool({description})` — documentation has ONE source, and the decorator's copy is
|
|
18
|
+
* what #984 deletes.
|
|
19
|
+
*/
|
|
20
|
+
readonly description: string;
|
|
21
|
+
/**
|
|
22
|
+
* All four hints. Three are COMPUTED from `operation` by `mcpHintsForOperation`, which is the
|
|
23
|
+
* one place that mapping lives; `openWorldHint` comes from `@Endpoint`'s `openWorld` option.
|
|
24
|
+
*/
|
|
25
|
+
readonly hints: WpMcpToolHints;
|
|
26
|
+
readonly inputSchema: ApiJsonSchema;
|
|
27
|
+
readonly outputSchema: ApiJsonSchema;
|
|
28
|
+
constructor(
|
|
29
|
+
/** The stable protocol name from `@WpMcpTool({name})`. */
|
|
30
|
+
name: string,
|
|
31
|
+
/** The contract method it was rendered from, so a mismatch report can name the source. */
|
|
32
|
+
methodName: string,
|
|
33
|
+
/**
|
|
34
|
+
* The tool documentation: the method's `@mcp` tag when it has one, its JSDoc body otherwise.
|
|
35
|
+
* NOT `@WpMcpTool({description})` — documentation has ONE source, and the decorator's copy is
|
|
36
|
+
* what #984 deletes.
|
|
37
|
+
*/
|
|
38
|
+
description: string,
|
|
39
|
+
/**
|
|
40
|
+
* All four hints. Three are COMPUTED from `operation` by `mcpHintsForOperation`, which is the
|
|
41
|
+
* one place that mapping lives; `openWorldHint` comes from `@Endpoint`'s `openWorld` option.
|
|
42
|
+
*/
|
|
43
|
+
hints: WpMcpToolHints, inputSchema: ApiJsonSchema, outputSchema: ApiJsonSchema);
|
|
44
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpToolDefinition = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* ONE MCP tool, as `tools/list` publishes it, rendered from the `ApiDocModel`.
|
|
6
|
+
*
|
|
7
|
+
* Every field here has a RUNTIME counterpart on `RegisteredMcpTool` in `@webpieces/mcp-server`, and
|
|
8
|
+
* the equivalence gate (#983) asserts the two are equal field by field. That is the whole reason this
|
|
9
|
+
* class mirrors that one's shape rather than inventing a tidier one: a difference in SHAPE would hide
|
|
10
|
+
* a difference in CONTENT, and the content is the thing being measured.
|
|
11
|
+
*/
|
|
12
|
+
class McpToolDefinition {
|
|
13
|
+
name;
|
|
14
|
+
methodName;
|
|
15
|
+
description;
|
|
16
|
+
hints;
|
|
17
|
+
inputSchema;
|
|
18
|
+
outputSchema;
|
|
19
|
+
constructor(
|
|
20
|
+
/** The stable protocol name from `@WpMcpTool({name})`. */
|
|
21
|
+
name,
|
|
22
|
+
/** The contract method it was rendered from, so a mismatch report can name the source. */
|
|
23
|
+
methodName,
|
|
24
|
+
/**
|
|
25
|
+
* The tool documentation: the method's `@mcp` tag when it has one, its JSDoc body otherwise.
|
|
26
|
+
* NOT `@WpMcpTool({description})` — documentation has ONE source, and the decorator's copy is
|
|
27
|
+
* what #984 deletes.
|
|
28
|
+
*/
|
|
29
|
+
description,
|
|
30
|
+
/**
|
|
31
|
+
* All four hints. Three are COMPUTED from `operation` by `mcpHintsForOperation`, which is the
|
|
32
|
+
* one place that mapping lives; `openWorldHint` comes from `@Endpoint`'s `openWorld` option.
|
|
33
|
+
*/
|
|
34
|
+
hints, inputSchema, outputSchema) {
|
|
35
|
+
this.name = name;
|
|
36
|
+
this.methodName = methodName;
|
|
37
|
+
this.description = description;
|
|
38
|
+
this.hints = hints;
|
|
39
|
+
this.inputSchema = inputSchema;
|
|
40
|
+
this.outputSchema = outputSchema;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.McpToolDefinition = McpToolDefinition;
|
|
44
|
+
//# sourceMappingURL=McpToolDefinition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"McpToolDefinition.js","sourceRoot":"","sources":["../../../../../../packages/docs/api-doc-model/src/render/McpToolDefinition.ts"],"names":[],"mappings":";;;AAEA;;;;;;;GAOG;AACH,MAAa,iBAAiB;IAGb;IAEA;IAMA;IAKA;IACA;IACA;IAjBb;IACI,0DAA0D;IACjD,IAAY;IACrB,0FAA0F;IACjF,UAAkB;IAC3B;;;;OAIG;IACM,WAAmB;IAC5B;;;OAGG;IACM,KAAqB,EACrB,WAA0B,EAC1B,YAA2B;QAf3B,SAAI,GAAJ,IAAI,CAAQ;QAEZ,eAAU,GAAV,UAAU,CAAQ;QAMlB,gBAAW,GAAX,WAAW,CAAQ;QAKnB,UAAK,GAAL,KAAK,CAAgB;QACrB,gBAAW,GAAX,WAAW,CAAe;QAC1B,iBAAY,GAAZ,YAAY,CAAe;IACrC,CAAC;CACP;AApBD,8CAoBC","sourcesContent":["import { ApiJsonSchema, WpMcpToolHints } from '@webpieces/core-util';\n\n/**\n * ONE MCP tool, as `tools/list` publishes it, rendered from the `ApiDocModel`.\n *\n * Every field here has a RUNTIME counterpart on `RegisteredMcpTool` in `@webpieces/mcp-server`, and\n * the equivalence gate (#983) asserts the two are equal field by field. That is the whole reason this\n * class mirrors that one's shape rather than inventing a tidier one: a difference in SHAPE would hide\n * a difference in CONTENT, and the content is the thing being measured.\n */\nexport class McpToolDefinition {\n constructor(\n /** The stable protocol name from `@WpMcpTool({name})`. */\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 * NOT `@WpMcpTool({description})` — documentation has ONE source, and the decorator's copy is\n * what #984 deletes.\n */\n readonly description: string,\n /**\n * All four hints. Three are COMPUTED from `operation` by `mcpHintsForOperation`, which is the\n * one place that mapping lives; `openWorldHint` comes from `@Endpoint`'s `openWorld` option.\n */\n readonly hints: WpMcpToolHints,\n readonly inputSchema: ApiJsonSchema,\n readonly outputSchema: ApiJsonSchema,\n ) {}\n}\n"]}
|