@webpieces/api-doc-model 0.4.806 → 0.4.807

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ApiDocModel.js","sourceRoot":"","sources":["../../../../../../packages/docs/api-doc-model/src/model/ApiDocModel.ts"],"names":[],"mappings":"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","sourcesContent":["import { TypeRef } from './TypeRef';\n\n/**\n * The model classes. Every one of them is a CLASS with an explicit constructor per `CLAUDE.md` §1 —\n * these are data-only structures, and a renderer (#982) builds nothing, it only reads.\n *\n * Nothing in this file imports anything but {@link TypeRef}: the package depends on `typescript` and\n * NOTHING else, so it can be pointed at any upstream project's contract. One app-specific import\n * here would end that, which is why `responsibilities.md` states the constraint rather than leaving\n * it to be discovered.\n */\n\n/** A type the extractor could not represent. RECORDED, never dropped — #982's guard needs a name. */\nexport class UnmappedType {\n constructor(\n /** The verbatim TypeScript type text, e.g. `Map<string, Widget>`. */\n readonly typeText: string,\n /** Pointer-style location: `path/to/File.ts:12:5`. */\n readonly location: string,\n /** Why it could not be mapped, in one sentence a renderer can print. */\n readonly reason: string,\n ) {}\n}\n\n/** The DERIVED discriminator of a union — never invented, only observed. See {@link DocumentedType}. */\nexport class UnionDiscriminator {\n constructor(\n /** The property every branch carries. */\n readonly propertyName: string,\n /** branch type name -> the single string literal that branch's property holds. */\n readonly branchValues: ReadonlyMap<string, string>,\n ) {}\n}\n\n/** One field of one DTO. */\nexport class DocumentedField {\n constructor(\n readonly name: string,\n readonly type: TypeRef,\n /**\n * OPTIONAL (`name?: string`) — the property may be ABSENT. Distinguished from\n * {@link nullable} on purpose: `{}` and `{name: null}` are different wire documents, and a\n * renderer that conflates them emits a schema that rejects one of them.\n */\n readonly optional: boolean,\n /** NULLABLE (`name: string | null`) — the property is present and may hold `null`. */\n readonly nullable: boolean,\n /** The JSDoc body, links flattened. Empty string when undocumented. */\n readonly description: string,\n /**\n * The `@mcp` block tag — an OPTIONAL agent-facing override. Undefined means \"no override\",\n * and a renderer falls back to {@link description}; the fallback is NOT applied here so a\n * renderer can tell a deliberate agent-facing sentence from a reused human one.\n */\n readonly mcpDescription: string | undefined,\n /** The `@format` tag, lifted onto the SCALAR (on an array, onto the ITEM). */\n readonly format: string | undefined,\n /** `@WpMin(n)` — numeric fields only; anything else is a build failure. */\n readonly min: number | undefined,\n /** `@WpMax(n)` — numeric fields only; anything else is a build failure. */\n readonly max: number | undefined,\n /**\n * The `@mcpHeader <token>` JSDoc tag — the MCP 2026 SEP-2243 header this PRIMITIVE field is\n * mirrored into (`Mcp-Param-{token}`). Undefined for every field that is not mirrored, which\n * is nearly all of them.\n *\n * It is documentation and therefore lives in JSDoc, next to the sentence describing the\n * field, rather than in a decorator argument. The runtime spells it `WpMcpHeader` inside\n * `@WpDtoField`; the two are proved identical by the equivalence gate (#983) before #984\n * deletes the decorator spelling.\n */\n readonly mcpHeader: string | undefined,\n ) {}\n}\n\n/** One named type reachable from a contract: an object DTO, a string-literal enum, or a union. */\nexport class DocumentedType {\n constructor(\n readonly name: string,\n readonly description: string,\n /** Object shape. Empty for an enum or a union. */\n readonly fields: readonly DocumentedField[],\n /** Set for a string-literal union that has a name. */\n readonly enumValues: readonly string[],\n /** Set for a union of named object types. */\n readonly unionRefNames: readonly string[],\n /** Set only when EVERY branch carries the same property typed as one string literal. */\n readonly discriminator: UnionDiscriminator | undefined,\n /** An index signature (`[k: string]: X`) — the OPEN-MAP half of an object that also has fields. */\n readonly indexSignatureValue: TypeRef | undefined,\n ) {}\n}\n\n/** `@Endpoint(httpMethod, path, operation, kind, options?)`'s LAST argument, as a document sees it. */\nexport class DocumentedEndpointOptions {\n constructor(\n readonly formPost: boolean,\n /** `calledBy` — REQUIRED by the decorator for `external`, absent otherwise. */\n readonly calledBy: string | undefined,\n readonly callerKind: string | undefined,\n ) {}\n}\n\n/**\n * The `@WpMcpTool(...)` declaration, when the method carries one — the TWO facts the source cannot\n * otherwise state, and nothing else.\n *\n * `description` is deliberately NOT read off the decorator. The method's JSDoc is the description, for\n * the agent and for the partner alike, byte-identical: two authored copies of one paragraph is the\n * two-spellings shim, and its failure mode is concrete — the partner reads the JSDoc in the OpenAPI\n * document while the agent reads the decorator string in `tools/list`, and they drift the first time\n * somebody edits one. (The field still exists on the decorator; #984 deletes it across its 29 call\n * sites. Nothing here reads it, so nothing here depends on a field that is about to disappear.)\n *\n * The three side-effect hints are not read either: they are COMPUTED from the endpoint's `operation`\n * by `mcpHintsForOperation` in `@webpieces/core-util`, which is the one place that mapping lives.\n * `READ | WRITE_IDEMPOTENT | WRITE` already says whether repeating a call is safe, so a hand-declared\n * hint would be a second answer to a question the contract has answered.\n */\nexport class DocumentedMcpTool {\n constructor(\n /**\n * The STABLE protocol name. Deliberately independent of the method name, because renaming a\n * method must not break a saved agent workflow.\n */\n readonly name: string,\n ) {}\n}\n\n/**\n * ONE credential of an api-key regime, PARSED — `{ in: 'header', name: 'x-api-key' }` or\n * `{ in: 'bearer' }`.\n *\n * Parsed rather than left as source text because it is the one auth argument a renderer must turn\n * into a STRUCTURE: an OpenAPI `securityScheme` is `{type: apiKey, in, name}` or\n * `{type: http, scheme: bearer}`, and those are different documents. A renderer handed the string\n * `\"{ in: 'header', name: 'x-api-key' }\"` would have to parse TypeScript to emit either one, which\n * is this package's job and not a renderer's.\n */\nexport class DocumentedApiKeyCredential {\n constructor(\n /** `header` or `bearer`, verbatim from the declaration. */\n readonly location: string,\n /** The header name. Undefined for `bearer`, whose location IS `Authorization`. */\n readonly name: string | undefined,\n /** The prose a docs site renders on its authorization card. */\n readonly description: string | undefined,\n ) {}\n}\n\n/** `@WpAuthApiKey(regime, credentials)`, parsed. See {@link DocumentedApiKeyCredential}. */\nexport class DocumentedApiKey {\n constructor(\n readonly regime: string,\n /**\n * Every credential the regime requires, IN DECLARATION ORDER. They are an AND — all of them\n * are presented together — and the order is the order a published document lists them in.\n */\n readonly credentials: readonly DocumentedApiKeyCredential[],\n ) {}\n}\n\n/** WHICH credential an endpoint demands — `@WpAuthPublic`, `@WpAuthJwt`, … — verbatim from the source. */\nexport class DocumentedAuth {\n constructor(\n /** The decorator name as written, e.g. `WpAuthJwt`. */\n readonly decorator: string,\n /** Every argument's text, verbatim and in order. Empty for `@WpAuthPublic()`. */\n readonly argumentTexts: readonly string[],\n /**\n * The PARSED api-key declaration, set only for `@WpAuthApiKey`. Every other decorator's\n * argument is prose or a role list that a document quotes rather than restructures, so\n * {@link argumentTexts} is all they need — see {@link DocumentedApiKeyCredential} for why\n * this one is different.\n */\n readonly apiKey: DocumentedApiKey | undefined,\n ) {}\n}\n\n/** One `@Endpoint` method of one contract. */\nexport class DocumentedEndpoint {\n constructor(\n readonly methodName: string,\n /**\n * `GET` or `POST`, constant-folded from `@Endpoint`'s FIRST argument. A document cannot be\n * written without it — the verb is the key an operation hangs under in `paths`.\n */\n readonly httpMethod: string,\n /** The path, constant-folded. A const that cannot be folded is a HARD FAILURE, never a guess. */\n readonly path: string,\n /**\n * `read` | `write-idempotent` | `write`, verbatim. The SIDE-EFFECT contract, which is\n * independent of the verb: webpieces POSTs a read. A renderer publishes it rather than\n * inferring safety from the verb, which for this framework would be wrong.\n */\n readonly operation: string,\n /** `rpc` | `cloudtasks` | `cron` | `external`, verbatim — this package invents no taxonomy. */\n readonly kind: string,\n /**\n * `{ hidden: true }` — this method is absent from the CUSTOMER document. It stays in the\n * private one, and in the MCP one when it is a tool. WHICH documents the CONTRACT feeds at\n * all is a different, class-level decision; see {@link ApiDocModel.apiTypes}.\n */\n readonly hidden: boolean,\n /** `{ openWorld: true }` — this operation may touch systems outside this service. */\n readonly openWorld: boolean,\n readonly options: DocumentedEndpointOptions,\n readonly auth: DocumentedAuth | undefined,\n readonly mcpTool: DocumentedMcpTool | undefined,\n /** `@WpMcpAuthJwt(...)`'s argument text, when present. */\n readonly mcpAuthText: string | undefined,\n /** `@MaskLog({...})` — field name -> mask mode. */\n readonly maskLog: ReadonlyMap<string, string>,\n readonly description: string,\n /** The `@mcp` override. See {@link DocumentedField.mcpDescription}. */\n readonly mcpDescription: string | undefined,\n readonly request: TypeRef | undefined,\n readonly response: TypeRef | undefined,\n ) {}\n}\n\n/** ONE extraction pass over ONE contract file. Both #982's renderers read exactly this. */\nexport class ApiDocModel {\n constructor(\n /** The contract class name, e.g. `SaveApi`. */\n readonly contractName: string,\n /**\n * WHICH generated documents this contract feeds — `svc-to-svc`, `external-customer`, `mcp`,\n * verbatim from `@ApiType(...)`, defaulting to `svc-to-svc` alone when it declares nothing.\n *\n * A named list rather than a falsy default, because the grant has to be the TOKEN: a default\n * that reached customers would publish a contract whose author typed nothing about it.\n */\n readonly apiTypes: readonly string[],\n /** `@ApiPath(...)`, constant-folded. */\n readonly basePath: string,\n /** The JSDoc on the contract class, links flattened. */\n readonly description: string,\n readonly endpoints: readonly DocumentedEndpoint[],\n /** Every named type reachable from the endpoints, by name. A `$ref` target for a renderer. */\n readonly types: ReadonlyMap<string, DocumentedType>,\n /** Everything that could not be represented — recorded, not dropped. */\n readonly unmapped: readonly UnmappedType[],\n ) {}\n}\n"]}
1
+ {"version":3,"file":"ApiDocModel.js","sourceRoot":"","sources":["../../../../../../packages/docs/api-doc-model/src/model/ApiDocModel.ts"],"names":[],"mappings":"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","sourcesContent":["import { TypeRef } from './TypeRef';\n\n/**\n * The model classes. Every one of them is a CLASS with an explicit constructor per `CLAUDE.md` §1 —\n * these are data-only structures, and a renderer (#982) builds nothing, it only reads.\n *\n * Nothing in this file imports anything but {@link TypeRef}: the package depends on `typescript` and\n * NOTHING else, so it can be pointed at any upstream project's contract. One app-specific import\n * here would end that, which is why `responsibilities.md` states the constraint rather than leaving\n * it to be discovered.\n */\n\n/** A type the extractor could not represent. RECORDED, never dropped — #982's guard needs a name. */\nexport class UnmappedType {\n constructor(\n /** The verbatim TypeScript type text, e.g. `Map<string, Widget>`. */\n readonly typeText: string,\n /** Pointer-style location: `path/to/File.ts:12:5`. */\n readonly location: string,\n /** Why it could not be mapped, in one sentence a renderer can print. */\n readonly reason: string,\n ) {}\n}\n\n/** The DERIVED discriminator of a union — never invented, only observed. See {@link DocumentedType}. */\nexport class UnionDiscriminator {\n constructor(\n /** The property every branch carries. */\n readonly propertyName: string,\n /** branch type name -> the single string literal that branch's property holds. */\n readonly branchValues: ReadonlyMap<string, string>,\n ) {}\n}\n\n/** One field of one DTO. */\nexport class DocumentedField {\n constructor(\n readonly name: string,\n readonly type: TypeRef,\n /**\n * OPTIONAL (`name?: string`) — the property may be ABSENT. Distinguished from\n * {@link nullable} on purpose: `{}` and `{name: null}` are different wire documents, and a\n * renderer that conflates them emits a schema that rejects one of them.\n */\n readonly optional: boolean,\n /** NULLABLE (`name: string | null`) — the property is present and may hold `null`. */\n readonly nullable: boolean,\n /** The JSDoc body, links flattened. Empty string when undocumented. */\n readonly description: string,\n /**\n * The `@mcp` block tag — an OPTIONAL agent-facing override. Undefined means \"no override\",\n * and a renderer falls back to {@link description}; the fallback is NOT applied here so a\n * renderer can tell a deliberate agent-facing sentence from a reused human one.\n */\n readonly mcpDescription: string | undefined,\n /** The `@format` tag, lifted onto the SCALAR (on an array, onto the ITEM). */\n readonly format: string | undefined,\n /** `@WpMin(n)` — numeric fields only; anything else is a build failure. */\n readonly min: number | undefined,\n /** `@WpMax(n)` — numeric fields only; anything else is a build failure. */\n readonly max: number | undefined,\n /**\n * The `@mcpHeader <token>` JSDoc tag — the MCP 2026 SEP-2243 header this PRIMITIVE field is\n * mirrored into (`Mcp-Param-{token}`). Undefined for every field that is not mirrored, which\n * is nearly all of them.\n *\n * It is documentation and therefore lives in JSDoc, next to the sentence describing the\n * field, rather than in a decorator argument — and since #984 it is the only spelling of the\n * fact at all.\n */\n readonly mcpHeader: string | undefined,\n ) {}\n}\n\n/** One named type reachable from a contract: an object DTO, a string-literal enum, or a union. */\nexport class DocumentedType {\n constructor(\n readonly name: string,\n readonly description: string,\n /** Object shape. Empty for an enum or a union. */\n readonly fields: readonly DocumentedField[],\n /** Set for a string-literal union that has a name. */\n readonly enumValues: readonly string[],\n /** Set for a union of named object types. */\n readonly unionRefNames: readonly string[],\n /** Set only when EVERY branch carries the same property typed as one string literal. */\n readonly discriminator: UnionDiscriminator | undefined,\n /** An index signature (`[k: string]: X`) — the OPEN-MAP half of an object that also has fields. */\n readonly indexSignatureValue: TypeRef | undefined,\n ) {}\n}\n\n/** `@Endpoint(httpMethod, path, operation, kind, options?)`'s LAST argument, as a document sees it. */\nexport class DocumentedEndpointOptions {\n constructor(\n readonly formPost: boolean,\n /** `calledBy` — REQUIRED by the decorator for `external`, absent otherwise. */\n readonly calledBy: string | undefined,\n readonly callerKind: string | undefined,\n ) {}\n}\n\n/**\n * The `@WpMcpTool('search_stores')` declaration, when the method carries one — the ONE fact the\n * source cannot otherwise state, and nothing else.\n *\n * `description` used to sit on the decorator too. The method's JSDoc is the description, for the\n * agent and for the partner alike, byte-identical: two authored copies of one paragraph is the\n * two-spellings shim, and its failure mode is concrete — the partner reads the JSDoc in the OpenAPI\n * document while the agent reads the decorator string in `tools/list`, and they drift the first time\n * somebody edits one. #984 deleted it, which left the decorator holding one field, so it takes the\n * name as a plain string.\n *\n * The three side-effect hints are COMPUTED from the endpoint's `operation` by `mcpHintsForOperation`\n * in `@webpieces/core-util`, which is the one place that mapping lives — `READ | WRITE_IDEMPOTENT |\n * WRITE` already says whether repeating a call is safe, so a hand-declared hint would be a second\n * answer to a question the contract has answered. `openWorldHint` is `@Endpoint`'s `openWorld`.\n */\nexport class DocumentedMcpTool {\n constructor(\n /**\n * The STABLE protocol name. Deliberately independent of the method name, because renaming a\n * method must not break a saved agent workflow.\n */\n readonly name: string,\n ) {}\n}\n\n/**\n * ONE credential of an api-key regime, PARSED — `{ in: 'header', name: 'x-api-key' }` or\n * `{ in: 'bearer' }`.\n *\n * Parsed rather than left as source text because it is the one auth argument a renderer must turn\n * into a STRUCTURE: an OpenAPI `securityScheme` is `{type: apiKey, in, name}` or\n * `{type: http, scheme: bearer}`, and those are different documents. A renderer handed the string\n * `\"{ in: 'header', name: 'x-api-key' }\"` would have to parse TypeScript to emit either one, which\n * is this package's job and not a renderer's.\n */\nexport class DocumentedApiKeyCredential {\n constructor(\n /** `header` or `bearer`, verbatim from the declaration. */\n readonly location: string,\n /** The header name. Undefined for `bearer`, whose location IS `Authorization`. */\n readonly name: string | undefined,\n /** The prose a docs site renders on its authorization card. */\n readonly description: string | undefined,\n ) {}\n}\n\n/** `@WpAuthApiKey(regime, credentials)`, parsed. See {@link DocumentedApiKeyCredential}. */\nexport class DocumentedApiKey {\n constructor(\n readonly regime: string,\n /**\n * Every credential the regime requires, IN DECLARATION ORDER. They are an AND — all of them\n * are presented together — and the order is the order a published document lists them in.\n */\n readonly credentials: readonly DocumentedApiKeyCredential[],\n ) {}\n}\n\n/** WHICH credential an endpoint demands — `@WpAuthPublic`, `@WpAuthJwt`, … — verbatim from the source. */\nexport class DocumentedAuth {\n constructor(\n /** The decorator name as written, e.g. `WpAuthJwt`. */\n readonly decorator: string,\n /** Every argument's text, verbatim and in order. Empty for `@WpAuthPublic()`. */\n readonly argumentTexts: readonly string[],\n /**\n * The PARSED api-key declaration, set only for `@WpAuthApiKey`. Every other decorator's\n * argument is prose or a role list that a document quotes rather than restructures, so\n * {@link argumentTexts} is all they need — see {@link DocumentedApiKeyCredential} for why\n * this one is different.\n */\n readonly apiKey: DocumentedApiKey | undefined,\n ) {}\n}\n\n/** One `@Endpoint` method of one contract. */\nexport class DocumentedEndpoint {\n constructor(\n readonly methodName: string,\n /**\n * `GET` or `POST`, constant-folded from `@Endpoint`'s FIRST argument. A document cannot be\n * written without it — the verb is the key an operation hangs under in `paths`.\n */\n readonly httpMethod: string,\n /** The path, constant-folded. A const that cannot be folded is a HARD FAILURE, never a guess. */\n readonly path: string,\n /**\n * `read` | `write-idempotent` | `write`, verbatim. The SIDE-EFFECT contract, which is\n * independent of the verb: webpieces POSTs a read. A renderer publishes it rather than\n * inferring safety from the verb, which for this framework would be wrong.\n */\n readonly operation: string,\n /** `rpc` | `cloudtasks` | `cron` | `external`, verbatim — this package invents no taxonomy. */\n readonly kind: string,\n /**\n * `{ hidden: true }` — this method is absent from the CUSTOMER document. It stays in the\n * private one, and in the MCP one when it is a tool. WHICH documents the CONTRACT feeds at\n * all is a different, class-level decision; see {@link ApiDocModel.apiTypes}.\n */\n readonly hidden: boolean,\n /** `{ openWorld: true }` — this operation may touch systems outside this service. */\n readonly openWorld: boolean,\n readonly options: DocumentedEndpointOptions,\n readonly auth: DocumentedAuth | undefined,\n readonly mcpTool: DocumentedMcpTool | undefined,\n /** `@WpMcpAuthJwt(...)`'s argument text, when present. */\n readonly mcpAuthText: string | undefined,\n /** `@MaskLog({...})` — field name -> mask mode. */\n readonly maskLog: ReadonlyMap<string, string>,\n readonly description: string,\n /** The `@mcp` override. See {@link DocumentedField.mcpDescription}. */\n readonly mcpDescription: string | undefined,\n readonly request: TypeRef | undefined,\n readonly response: TypeRef | undefined,\n ) {}\n}\n\n/** ONE extraction pass over ONE contract file. Both #982's renderers read exactly this. */\nexport class ApiDocModel {\n constructor(\n /** The contract class name, e.g. `SaveApi`. */\n readonly contractName: string,\n /**\n * WHICH generated documents this contract feeds — `svc-to-svc`, `external-customer`, `mcp`,\n * verbatim from `@ApiType(...)`, defaulting to `svc-to-svc` alone when it declares nothing.\n *\n * A named list rather than a falsy default, because the grant has to be the TOKEN: a default\n * that reached customers would publish a contract whose author typed nothing about it.\n */\n readonly apiTypes: readonly string[],\n /** `@ApiPath(...)`, constant-folded. */\n readonly basePath: string,\n /** The JSDoc on the contract class, links flattened. */\n readonly description: string,\n readonly endpoints: readonly DocumentedEndpoint[],\n /** Every named type reachable from the endpoints, by name. A `$ref` target for a renderer. */\n readonly types: ReadonlyMap<string, DocumentedType>,\n /** Everything that could not be represented — recorded, not dropped. */\n readonly unmapped: readonly UnmappedType[],\n ) {}\n}\n"]}
@@ -1,41 +1,79 @@
1
- import { ApiDocModel } from '../model/ApiDocModel';
2
- import { McpToolDefinition } from './McpToolDefinition';
1
+ import { McpToolCatalog, McpToolDefinition } from '@webpieces/core-util';
2
+ import { ApiDocModel, DocumentedEndpoint } from '../model/ApiDocModel';
3
+ /** ONE `@WpMcpTool` the build could not give a schema, and why. See {@link McpSchemaRenderer.catalogOf}. */
4
+ export declare class SkippedMcpTool {
5
+ /** The stable protocol name it declared. */
6
+ readonly name: string;
7
+ readonly contractName: string;
8
+ /** The render refusal, verbatim, including the declaration it points at. */
9
+ readonly reason: string;
10
+ constructor(
11
+ /** The stable protocol name it declared. */
12
+ name: string, contractName: string,
13
+ /** The render refusal, verbatim, including the declaration it points at. */
14
+ reason: string);
15
+ toString(): string;
16
+ }
17
+ /** What one catalog render produced: the tools, and the ones it could not produce. */
18
+ export declare class McpCatalogRender {
19
+ readonly catalog: McpToolCatalog;
20
+ readonly skipped: readonly SkippedMcpTool[];
21
+ constructor(catalog: McpToolCatalog, skipped: readonly SkippedMcpTool[]);
22
+ }
3
23
  /**
4
- * `ApiDocModel` -> the MCP tool list, in exactly the shape `DtoSchemaBuilder` produces at boot from
5
- * reflect-metadata.
6
- *
7
- * ## Why this exists at all, and why it is READ-ONLY
24
+ * `ApiDocModel` -> the MCP tool list an agent is shown, and the server accepts calls against.
8
25
  *
9
- * #984 wants the MCP runtime off reflect-metadata, which deletes every erasure-repair argument of
10
- * `@WpDtoField` — `required`, `arrayItems`, `integer`, `minimum`, `maximum`, `enumValues`. That is a
11
- * behaviour change on a live protocol surface, and its failure mode is silent: a tool whose input
12
- * schema quietly loses a `required` entry or an `enum` starts failing agent calls at RUNTIME, not at
13
- * build. This renderer plus the equivalence spec beside it turn "the compiler can obviously replace
14
- * those arguments" from a plausible argument into a MEASURED one (#983).
26
+ * ## It is the ONLY source of an MCP schema
15
27
  *
16
- * ## It deliberately reproduces `DtoSchemaBuilder`, defect-for-defect
28
+ * It was written for #983 as a MEASUREMENT: it rendered the same `ApiJsonSchema` the reflect-metadata
29
+ * runtime built, so the two could be compared field by field. They matched for every DTO shape the
30
+ * runtime could build, which is what licensed #984 to delete `@WpDtoField` and its erasure-repair
31
+ * arguments — `required`, `arrayItems`, `integer`, `minimum`, `maximum`, `enumValues`, `mapValues`,
32
+ * `mcpHeader` — along with `DtoSchemaBuilder` itself. There is now one schema, built here, written to
33
+ * `mcp-tools.json` by `wp-openapi`, and read at boot by `McpToolRegistry`.
17
34
  *
18
- * Where MCP's `ApiJsonSchema` subset could express MORE than the runtime does, this renderer emits
19
- * what the RUNTIME emits, and the gate's report names the difference. Three concrete cases:
35
+ * ## Where it now goes FURTHER than the deleted runtime could
20
36
  *
21
- * - **NULLABLE is not rendered.** `ApiJsonSchema.type` holds one string, so `type: [T, "null"]` is
22
- * not expressible in the type the runtime publishes. The compiler can SEE `externalId: string | null`
23
- * and the runtime cannot; emitting it here would fail the gate on a difference that is a runtime
24
- * capability GAP rather than an extractor bug, and hide the real mismatches under it.
25
- * - **A nested DTO is INLINED**, with the FIELD's prose on it, because `buildAt` inlines and then
26
- * `fieldSchema` overwrites `description`. MCP has no `$ref`.
27
- * - **A bound on an ARRAY of numbers** is put on the ITEM, where OpenAPI puts it; the runtime cannot
28
- * express it at all (`@WpDtoField` rejects numeric constraints on a non-`Number` field).
37
+ * The three reproductions #983 documented were runtime capability gaps, and two of them are closed:
29
38
  *
30
- * Making the comparison agree by WEAKENING it would destroy the only thing the gate is for, so every
31
- * one of those is a documented, deliberate reproduction rather than a relaxation.
39
+ * - **NULLABLE is rendered**, as `type: [T, "null"]`. `ApiJsonSchema.type` used to hold one string,
40
+ * so the runtime could not write it down even where the compiler could see `externalId: string | null`
41
+ * — and `{}` and `{externalId: null}` are different wire documents. `type` is now
42
+ * `ApiJsonSchemaType | readonly ApiJsonSchemaType[]` and the union is emitted.
43
+ * - **A bound on an ARRAY of numbers** lands on the ITEM, where OpenAPI puts it. `@WpDtoField`
44
+ * rejected numeric constraints on a non-`Number` field, so it had nowhere to go at all.
45
+ * - **A nested DTO is INLINED**, with the FIELD's prose on it — not a gap but the protocol: MCP tool
46
+ * schemas are inline and have no `$ref`.
32
47
  */
33
48
  export declare class McpSchemaRenderer {
34
49
  private readonly model;
35
50
  constructor(model: ApiDocModel);
36
51
  /** Every `@WpMcpTool` method of the contract, in declaration order. */
37
52
  render(): readonly McpToolDefinition[];
38
- private tool;
53
+ /**
54
+ * The tools of SEVERAL contracts as one catalog — the artifact a server boots from — plus every
55
+ * tool that could not be rendered and why.
56
+ *
57
+ * A catalog rather than a per-contract list because the protocol namespace is flat: two contracts
58
+ * declaring one tool name is a collision an agent would see, and {@link McpToolCatalog} refuses it
59
+ * here, at build time, rather than at somebody's boot.
60
+ *
61
+ * ## Why an unrenderable tool is REPORTED here rather than throwing
62
+ *
63
+ * Some shapes have no MCP schema at all — a discriminated union, a recursive DTO — and that is a
64
+ * limit of the PROTOCOL, not a defect in a contract that is otherwise perfectly good HTTP. Such a
65
+ * tool was never servable: the reflect-metadata runtime refused it too, for its own reasons. So
66
+ * failing the whole document build over one would stop a repo publishing its OpenAPI over a tool
67
+ * nobody could ever have called.
68
+ *
69
+ * It is not silence either. The build NAMES every skipped tool with its reason, and
70
+ * `McpToolRegistry` REFUSES TO BOOT when a registered `@WpMcpTool` is missing from the catalog —
71
+ * which is the right place for that failure, because that is the process actually claiming to
72
+ * serve it.
73
+ */
74
+ static catalogOf(models: readonly ApiDocModel[]): McpCatalogRender;
75
+ /** ONE tool. Visible to {@link catalogOf}, which renders tool by tool so it can report one. */
76
+ tool(endpoint: DocumentedEndpoint): McpToolDefinition;
39
77
  /**
40
78
  * `operation` verbatim from the model, mapped back onto the REAL constant.
41
79
  *
@@ -44,27 +82,33 @@ export declare class McpSchemaRenderer {
44
82
  * are exhaustive, means falling off the end and publishing a tool with NO hints.
45
83
  */
46
84
  private static operationOf;
47
- /** A tool's input/output schema: always a closed object, exactly as `DtoSchemaBuilder.build` is. */
85
+ /** A tool's input/output schema: always a CLOSED object, because MCP publishes objects. */
48
86
  private rootSchema;
49
87
  private namedType;
50
88
  /**
51
- * One object DTO. CLOSED (`additionalProperties: false`), `required` only when non-empty — both
52
- * exactly as `DtoSchemaBuilder.buildAt` writes them, because that is what is being compared.
89
+ * One object DTO. CLOSED (`additionalProperties: false`), with `required` written only when it is
90
+ * non-empty, so an all-optional DTO carries no empty list.
53
91
  *
54
- * The cycle stop is `parents`, and it THROWS rather than truncating, which is again what the
55
- * runtime does: an inline schema cannot express a recursive DTO, and one that silently stopped a
56
- * level down would publish a shape the server does not accept.
92
+ * The cycle stop is `parents`, and it THROWS rather than truncating: an inline schema cannot
93
+ * express a recursive DTO, and one that silently stopped a level down would publish a shape the
94
+ * server does not accept.
57
95
  */
58
96
  private objectSchema;
59
97
  /**
60
98
  * One FIELD: its type, then the prose and the constraints that hang off the field.
61
99
  *
62
- * The ORDER matters and mirrors `fieldSchema`: the type is built first and `description` is
63
- * written over whatever the type produced, so a nested DTO carries the FIELD's sentence rather
64
- * than the DTO's. `@WpMin` / `@WpMax` land on the numeric LEAF — on an array, on the item — for
65
- * the same reason `SchemaRenderer` puts them there: a `minimum` on an array means nothing.
100
+ * The ORDER matters: the type is built first and `description` is written over whatever the type
101
+ * produced, so a nested DTO carries the FIELD's sentence rather than the DTO's. `@WpMin` /
102
+ * `@WpMax` land on the numeric LEAF — on an array, on the item — for the same reason
103
+ * `SchemaRenderer` puts them there: a `minimum` on an array means nothing.
104
+ *
105
+ * NULLABLE widens the type to `[T, "null"]` and is deliberately NOT the same thing as OPTIONAL,
106
+ * which is an absent entry in the object's `required` list. `{}` and `{externalId: null}` are
107
+ * different wire documents, and an agent told only "optional" would send the wrong one.
66
108
  */
67
109
  private fieldSchema;
110
+ /** `T` -> `[T, "null"]`, refusing a field with no type at all rather than publishing `["null"]`. */
111
+ private static nullable;
68
112
  /**
69
113
  * `@mcpHeader` mirrors a PRIMITIVE parameter into `Mcp-Param-{name}` (MCP 2026 SEP-2243). The
70
114
  * three conditions are the runtime's, restated against the compiler's view of the field, so a
@@ -1,40 +1,63 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.McpSchemaRenderer = void 0;
3
+ exports.McpSchemaRenderer = exports.McpCatalogRender = exports.SkippedMcpTool = void 0;
4
4
  const core_util_1 = require("@webpieces/core-util");
5
5
  const McpRenderError_1 = require("./McpRenderError");
6
- const McpToolDefinition_1 = require("./McpToolDefinition");
7
6
  /** RFC 9110 token, which is what an `Mcp-Param-{name}` header name has to be. */
8
7
  const HEADER_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
8
+ /** ONE `@WpMcpTool` the build could not give a schema, and why. See {@link McpSchemaRenderer.catalogOf}. */
9
+ class SkippedMcpTool {
10
+ name;
11
+ contractName;
12
+ reason;
13
+ constructor(
14
+ /** The stable protocol name it declared. */
15
+ name, contractName,
16
+ /** The render refusal, verbatim, including the declaration it points at. */
17
+ reason) {
18
+ this.name = name;
19
+ this.contractName = contractName;
20
+ this.reason = reason;
21
+ }
22
+ toString() {
23
+ return `${this.contractName}/${this.name}: ${this.reason}`;
24
+ }
25
+ }
26
+ exports.SkippedMcpTool = SkippedMcpTool;
27
+ /** What one catalog render produced: the tools, and the ones it could not produce. */
28
+ class McpCatalogRender {
29
+ catalog;
30
+ skipped;
31
+ constructor(catalog, skipped) {
32
+ this.catalog = catalog;
33
+ this.skipped = skipped;
34
+ }
35
+ }
36
+ exports.McpCatalogRender = McpCatalogRender;
9
37
  /**
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
38
+ * `ApiDocModel` -> the MCP tool list an agent is shown, and the server accepts calls against.
14
39
  *
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).
40
+ * ## It is the ONLY source of an MCP schema
21
41
  *
22
- * ## It deliberately reproduces `DtoSchemaBuilder`, defect-for-defect
42
+ * It was written for #983 as a MEASUREMENT: it rendered the same `ApiJsonSchema` the reflect-metadata
43
+ * runtime built, so the two could be compared field by field. They matched for every DTO shape the
44
+ * runtime could build, which is what licensed #984 to delete `@WpDtoField` and its erasure-repair
45
+ * arguments — `required`, `arrayItems`, `integer`, `minimum`, `maximum`, `enumValues`, `mapValues`,
46
+ * `mcpHeader` — along with `DtoSchemaBuilder` itself. There is now one schema, built here, written to
47
+ * `mcp-tools.json` by `wp-openapi`, and read at boot by `McpToolRegistry`.
23
48
  *
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:
49
+ * ## Where it now goes FURTHER than the deleted runtime could
26
50
  *
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).
51
+ * The three reproductions #983 documented were runtime capability gaps, and two of them are closed:
35
52
  *
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.
53
+ * - **NULLABLE is rendered**, as `type: [T, "null"]`. `ApiJsonSchema.type` used to hold one string,
54
+ * so the runtime could not write it down even where the compiler could see `externalId: string | null`
55
+ * — and `{}` and `{externalId: null}` are different wire documents. `type` is now
56
+ * `ApiJsonSchemaType | readonly ApiJsonSchemaType[]` and the union is emitted.
57
+ * - **A bound on an ARRAY of numbers** lands on the ITEM, where OpenAPI puts it. `@WpDtoField`
58
+ * rejected numeric constraints on a non-`Number` field, so it had nowhere to go at all.
59
+ * - **A nested DTO is INLINED**, with the FIELD's prose on it — not a gap but the protocol: MCP tool
60
+ * schemas are inline and have no `$ref`.
38
61
  */
39
62
  class McpSchemaRenderer {
40
63
  model;
@@ -51,6 +74,52 @@ class McpSchemaRenderer {
51
74
  }
52
75
  return tools;
53
76
  }
77
+ /**
78
+ * The tools of SEVERAL contracts as one catalog — the artifact a server boots from — plus every
79
+ * tool that could not be rendered and why.
80
+ *
81
+ * A catalog rather than a per-contract list because the protocol namespace is flat: two contracts
82
+ * declaring one tool name is a collision an agent would see, and {@link McpToolCatalog} refuses it
83
+ * here, at build time, rather than at somebody's boot.
84
+ *
85
+ * ## Why an unrenderable tool is REPORTED here rather than throwing
86
+ *
87
+ * Some shapes have no MCP schema at all — a discriminated union, a recursive DTO — and that is a
88
+ * limit of the PROTOCOL, not a defect in a contract that is otherwise perfectly good HTTP. Such a
89
+ * tool was never servable: the reflect-metadata runtime refused it too, for its own reasons. So
90
+ * failing the whole document build over one would stop a repo publishing its OpenAPI over a tool
91
+ * nobody could ever have called.
92
+ *
93
+ * It is not silence either. The build NAMES every skipped tool with its reason, and
94
+ * `McpToolRegistry` REFUSES TO BOOT when a registered `@WpMcpTool` is missing from the catalog —
95
+ * which is the right place for that failure, because that is the process actually claiming to
96
+ * serve it.
97
+ */
98
+ // webpieces-disable no-function-outside-class -- static factory over this class
99
+ static catalogOf(models) {
100
+ const tools = [];
101
+ const skipped = [];
102
+ for (const model of models) {
103
+ const renderer = new McpSchemaRenderer(model);
104
+ for (const endpoint of model.endpoints) {
105
+ if (endpoint.mcpTool === undefined) {
106
+ continue;
107
+ }
108
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a per-tool render refusal is REPORTED, see the docstring
109
+ try {
110
+ tools.push(renderer.tool(endpoint));
111
+ }
112
+ catch (err) {
113
+ //const error = toError(err);
114
+ if (!(err instanceof McpRenderError_1.McpRenderError))
115
+ throw err;
116
+ skipped.push(new SkippedMcpTool(endpoint.mcpTool.name, model.contractName, err.message));
117
+ }
118
+ }
119
+ }
120
+ return new McpCatalogRender(new core_util_1.McpToolCatalog(tools), skipped);
121
+ }
122
+ /** ONE tool. Visible to {@link catalogOf}, which renders tool by tool so it can report one. */
54
123
  tool(endpoint) {
55
124
  const where = `${this.model.contractName}.${endpoint.methodName}`;
56
125
  const description = endpoint.mcpDescription ?? endpoint.description;
@@ -61,7 +130,7 @@ class McpSchemaRenderer {
61
130
  if (endpoint.request === undefined || endpoint.response === undefined) {
62
131
  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
132
  }
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'));
133
+ return new core_util_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
134
  }
66
135
  /**
67
136
  * `operation` verbatim from the model, mapped back onto the REAL constant.
@@ -83,7 +152,7 @@ class McpSchemaRenderer {
83
152
  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
153
  }
85
154
  }
86
- /** A tool's input/output schema: always a closed object, exactly as `DtoSchemaBuilder.build` is. */
155
+ /** A tool's input/output schema: always a CLOSED object, because MCP publishes objects. */
87
156
  rootSchema(ref, where, side) {
88
157
  if (ref.kind !== 'ref') {
89
158
  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 ' +
@@ -99,12 +168,12 @@ class McpSchemaRenderer {
99
168
  return type;
100
169
  }
101
170
  /**
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.
171
+ * One object DTO. CLOSED (`additionalProperties: false`), with `required` written only when it is
172
+ * non-empty, so an all-optional DTO carries no empty list.
104
173
  *
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.
174
+ * The cycle stop is `parents`, and it THROWS rather than truncating: an inline schema cannot
175
+ * express a recursive DTO, and one that silently stopped a level down would publish a shape the
176
+ * server does not accept.
108
177
  */
109
178
  objectSchema(type, parents) {
110
179
  if (parents.has(type.name)) {
@@ -138,10 +207,14 @@ class McpSchemaRenderer {
138
207
  /**
139
208
  * One FIELD: its type, then the prose and the constraints that hang off the field.
140
209
  *
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.
210
+ * The ORDER matters: the type is built first and `description` is written over whatever the type
211
+ * produced, so a nested DTO carries the FIELD's sentence rather than the DTO's. `@WpMin` /
212
+ * `@WpMax` land on the numeric LEAF — on an array, on the item — for the same reason
213
+ * `SchemaRenderer` puts them there: a `minimum` on an array means nothing.
214
+ *
215
+ * NULLABLE widens the type to `[T, "null"]` and is deliberately NOT the same thing as OPTIONAL,
216
+ * which is an absent entry in the object's `required` list. `{}` and `{externalId: null}` are
217
+ * different wire documents, and an agent told only "optional" would send the wrong one.
145
218
  */
146
219
  fieldSchema(owner, field, parents) {
147
220
  const where = `${owner.name}.${field.name}`;
@@ -163,8 +236,20 @@ class McpSchemaRenderer {
163
236
  McpSchemaRenderer.assertHeaderFits(field, schema, where);
164
237
  schema['x-mcp-header'] = field.mcpHeader;
165
238
  }
239
+ if (field.nullable) {
240
+ schema.type = McpSchemaRenderer.nullable(schema, where);
241
+ }
166
242
  return schema;
167
243
  }
244
+ /** `T` -> `[T, "null"]`, refusing a field with no type at all rather than publishing `["null"]`. */
245
+ // webpieces-disable no-function-outside-class -- private static mapping of this class
246
+ static nullable(schema, where) {
247
+ const base = core_util_1.ApiJsonSchema.baseTypeOf(schema);
248
+ if (base === undefined) {
249
+ throw new McpRenderError_1.McpRenderError('a nullable field has no other type', where, 'A field typed only `null` carries no information — give it a real type beside it.');
250
+ }
251
+ return [base, 'null'];
252
+ }
168
253
  /**
169
254
  * `@mcpHeader` mirrors a PRIMITIVE parameter into `Mcp-Param-{name}` (MCP 2026 SEP-2243). The
170
255
  * three conditions are the runtime's, restated against the compiler's view of the field, so a
@@ -1 +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"]}
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 ApiJsonSchemaType,\n EndpointOperation,\n McpToolCatalog,\n McpToolDefinition,\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';\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/** ONE `@WpMcpTool` the build could not give a schema, and why. See {@link McpSchemaRenderer.catalogOf}. */\nexport class SkippedMcpTool {\n constructor(\n /** The stable protocol name it declared. */\n readonly name: string,\n readonly contractName: string,\n /** The render refusal, verbatim, including the declaration it points at. */\n readonly reason: string,\n ) {}\n\n toString(): string {\n return `${this.contractName}/${this.name}: ${this.reason}`;\n }\n}\n\n/** What one catalog render produced: the tools, and the ones it could not produce. */\nexport class McpCatalogRender {\n constructor(\n readonly catalog: McpToolCatalog,\n readonly skipped: readonly SkippedMcpTool[],\n ) {}\n}\n\n/**\n * `ApiDocModel` -> the MCP tool list an agent is shown, and the server accepts calls against.\n *\n * ## It is the ONLY source of an MCP schema\n *\n * It was written for #983 as a MEASUREMENT: it rendered the same `ApiJsonSchema` the reflect-metadata\n * runtime built, so the two could be compared field by field. They matched for every DTO shape the\n * runtime could build, which is what licensed #984 to delete `@WpDtoField` and its erasure-repair\n * arguments — `required`, `arrayItems`, `integer`, `minimum`, `maximum`, `enumValues`, `mapValues`,\n * `mcpHeader` — along with `DtoSchemaBuilder` itself. There is now one schema, built here, written to\n * `mcp-tools.json` by `wp-openapi`, and read at boot by `McpToolRegistry`.\n *\n * ## Where it now goes FURTHER than the deleted runtime could\n *\n * The three reproductions #983 documented were runtime capability gaps, and two of them are closed:\n *\n * - **NULLABLE is rendered**, as `type: [T, \"null\"]`. `ApiJsonSchema.type` used to hold one string,\n * so the runtime could not write it down even where the compiler could see `externalId: string | null`\n * — and `{}` and `{externalId: null}` are different wire documents. `type` is now\n * `ApiJsonSchemaType | readonly ApiJsonSchemaType[]` and the union is emitted.\n * - **A bound on an ARRAY of numbers** lands on the ITEM, where OpenAPI puts it. `@WpDtoField`\n * rejected numeric constraints on a non-`Number` field, so it had nowhere to go at all.\n * - **A nested DTO is INLINED**, with the FIELD's prose on it — not a gap but the protocol: MCP tool\n * schemas are inline and have no `$ref`.\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 /**\n * The tools of SEVERAL contracts as one catalog — the artifact a server boots from — plus every\n * tool that could not be rendered and why.\n *\n * A catalog rather than a per-contract list because the protocol namespace is flat: two contracts\n * declaring one tool name is a collision an agent would see, and {@link McpToolCatalog} refuses it\n * here, at build time, rather than at somebody's boot.\n *\n * ## Why an unrenderable tool is REPORTED here rather than throwing\n *\n * Some shapes have no MCP schema at all — a discriminated union, a recursive DTO — and that is a\n * limit of the PROTOCOL, not a defect in a contract that is otherwise perfectly good HTTP. Such a\n * tool was never servable: the reflect-metadata runtime refused it too, for its own reasons. So\n * failing the whole document build over one would stop a repo publishing its OpenAPI over a tool\n * nobody could ever have called.\n *\n * It is not silence either. The build NAMES every skipped tool with its reason, and\n * `McpToolRegistry` REFUSES TO BOOT when a registered `@WpMcpTool` is missing from the catalog —\n * which is the right place for that failure, because that is the process actually claiming to\n * serve it.\n */\n // webpieces-disable no-function-outside-class -- static factory over this class\n static catalogOf(models: readonly ApiDocModel[]): McpCatalogRender {\n const tools: McpToolDefinition[] = [];\n const skipped: SkippedMcpTool[] = [];\n for (const model of models) {\n const renderer = new McpSchemaRenderer(model);\n for (const endpoint of model.endpoints) {\n if (endpoint.mcpTool === undefined) {\n continue;\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a per-tool render refusal is REPORTED, see the docstring\n try {\n tools.push(renderer.tool(endpoint));\n } catch (err: unknown) {\n //const error = toError(err);\n if (!(err instanceof McpRenderError)) throw err;\n skipped.push(\n new SkippedMcpTool(endpoint.mcpTool.name, model.contractName, err.message),\n );\n }\n }\n }\n return new McpCatalogRender(new McpToolCatalog(tools), skipped);\n }\n\n /** ONE tool. Visible to {@link catalogOf}, which renders tool by tool so it can report one. */\n 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, because MCP publishes objects. */\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`), with `required` written only when it is\n * non-empty, so an all-optional DTO carries no empty list.\n *\n * The cycle stop is `parents`, and it THROWS rather than truncating: an inline schema cannot\n * express a recursive DTO, and one that silently stopped a level down would publish a shape the\n * 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: the type is built first and `description` is written over whatever the type\n * produced, so a nested DTO carries the FIELD's sentence rather than the DTO's. `@WpMin` /\n * `@WpMax` land on the numeric LEAF — on an array, on the item — for the same reason\n * `SchemaRenderer` puts them there: a `minimum` on an array means nothing.\n *\n * NULLABLE widens the type to `[T, \"null\"]` and is deliberately NOT the same thing as OPTIONAL,\n * which is an absent entry in the object's `required` list. `{}` and `{externalId: null}` are\n * different wire documents, and an agent told only \"optional\" would send the wrong one.\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 if (field.nullable) {\n schema.type = McpSchemaRenderer.nullable(schema, where);\n }\n return schema;\n }\n\n /** `T` -> `[T, \"null\"]`, refusing a field with no type at all rather than publishing `[\"null\"]`. */\n // webpieces-disable no-function-outside-class -- private static mapping of this class\n private static nullable(schema: ApiJsonSchema, where: string): readonly ApiJsonSchemaType[] {\n const base = ApiJsonSchema.baseTypeOf(schema);\n if (base === undefined) {\n throw new McpRenderError(\n 'a nullable field has no other type',\n where,\n 'A field typed only `null` carries no information — give it a real type beside it.',\n );\n }\n return [base, 'null'];\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"]}
@@ -1,44 +0,0 @@
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
- }