@webpieces/core-util 0.4.881 → 0.4.882
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/http/api-type.js +2 -2
- package/src/http/api-type.js.map +1 -1
- package/src/mcp/McpCompileAssertions.js +14 -4
- package/src/mcp/McpCompileAssertions.js.map +1 -1
- package/src/mcp/McpMetadata.d.ts +25 -13
- package/src/mcp/McpMetadata.js +31 -14
- package/src/mcp/McpMetadata.js.map +1 -1
- package/src/mcp/McpToolCatalogFile.d.ts +14 -2
- package/src/mcp/McpToolCatalogFile.js +24 -3
- package/src/mcp/McpToolCatalogFile.js.map +1 -1
package/package.json
CHANGED
package/src/http/api-type.js
CHANGED
|
@@ -102,8 +102,8 @@ function assertApiTypeMatchesMcpTools(apiClass) {
|
|
|
102
102
|
'every method is permanently outside MCP does not feed the MCP document.');
|
|
103
103
|
}
|
|
104
104
|
throw new Error(`${apiClass.name} declares @ApiType(..., MCP) but no method carries @WpMcpTool. ` +
|
|
105
|
-
"Add @WpMcpTool('<stable_tool_name>') to the methods
|
|
106
|
-
'from the @ApiType list.');
|
|
105
|
+
"Add @WpMcpTool('<stable_tool_name>', '<Human-readable title>') to the methods " +
|
|
106
|
+
'agents may call, or drop MCP from the @ApiType list.');
|
|
107
107
|
}
|
|
108
108
|
if (!declaresMcp && tools.length > 0) {
|
|
109
109
|
const named = tools.map((tool) => tool.methodName).join(', ');
|
package/src/http/api-type.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-type.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/api-type.ts"],"names":[],"mappings":"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","sourcesContent":["import 'reflect-metadata';\nimport { METADATA_KEYS } from './decorators';\nimport {\n InvalidEndpointForMcpMetadata,\n getInvalidEndpointsForMcp,\n getWpMcpTools,\n WpMcpToolMetadata,\n} from '../mcp/McpMetadata';\n\n/** Nominal backing keeps raw string literals out of contract declarations. */\nenum ApiTypeValue {\n SVC_TO_SVC = 'svc-to-svc',\n EXTERNAL_CUSTOMER = 'external-customer',\n MCP = 'mcp',\n}\n\n/** Short, statically importable decorator arguments. */\nexport const SVC_TO_SVC = ApiTypeValue.SVC_TO_SVC;\nexport const EXTERNAL_CUSTOMER = ApiTypeValue.EXTERNAL_CUSTOMER;\nexport const MCP = ApiTypeValue.MCP;\n\n/** WHICH generated documents a contract feeds. One value per document. */\nexport type ApiTypeKind = typeof SVC_TO_SVC | typeof EXTERNAL_CUSTOMER | typeof MCP;\n\n/** Metadata key for the contract-level declaration, parallel to {@link METADATA_KEYS}. */\nexport const API_TYPE_METADATA_KEY = 'webpieces:api-type';\n\n/** The fail-closed default: a contract that says nothing feeds only the internal document. */\nexport const DEFAULT_API_TYPES: readonly ApiTypeKind[] = [SVC_TO_SVC];\n\n/**\n * WHICH generated documents this CONTRACT feeds.\n *\n * ```typescript\n * @ApiPath('/stores')\n * @ApiType(SVC_TO_SVC, EXTERNAL_CUSTOMER) // private + customer documents\n * export abstract class StoreApi { ... }\n * ```\n *\n * | value | document |\n * |---|---|\n * | `SVC_TO_SVC` | `full-private-openapi.json` |\n * | `EXTERNAL_CUSTOMER` | `public-openapi.json` |\n * | `MCP` | `mcp-openapi.json` |\n *\n * A document is WRITTEN when at least one contract declares its type, and it contains the contracts\n * that declared it and no others. No contract declaring `MCP` therefore means no `mcp-openapi.json`\n * at all — there is no separate emptiness rule to keep in step with this one.\n *\n * ## Absent means `SVC_TO_SVC` only, and the level is why that is affordable\n *\n * `.claude/rules/no-backwards-compat.md` shim shape #5 rejects \"a widening that is an ABSENCE rather\n * than a token\": a default that publishes to customers would mean you reach them having typed\n * nothing, so the safe state requires an edit and the widest grant is free. Naming the type inverts\n * that — `grep -rn EXTERNAL_CUSTOMER` is the entire customer-facing surface on one screen.\n *\n * It is a CLASS-level declaration because that costs one token per contract rather than one per\n * method. A per-method audience list was the earlier design and was rejected on exactly that\n * ergonomic ground; the per-METHOD case that remains — a new endpoint on an already-published API —\n * is served by `{ hidden: true }` in `EndpointOptions`, which subtracts one method from the customer\n * document and leaves it in the other two. The two are orthogonal: this one selects FILES, that one\n * subtracts a METHOD from one file.\n *\n * ## `MCP` and `@WpMcpTool` are ONE decision\n *\n * `MCP` on a contract with no `@WpMcpTool` method is an error, and a `@WpMcpTool` method on a\n * contract that does not declare `MCP` is an error. Both directions are asserted by\n * {@link assertApiTypeMatchesMcpTools}; without that, membership is declared in two places and they\n * can disagree, which is the defect this whole documentation epic exists to remove.\n *\n * `@InvalidEndpointForMcp` sits OUTSIDE that biconditional: a method declared permanently unusable\n * as a tool is neither required to carry `@WpMcpTool` nor permitted to. A contract on which EVERY\n * endpoint is so declared must not declare `MCP` at all, and that too is asserted there.\n */\n// webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope\nexport function ApiType(first: ApiTypeKind, ...rest: readonly ApiTypeKind[]): ClassDecorator {\n const types = [first, ...rest];\n return (target: Function) => {\n Reflect.defineMetadata(API_TYPE_METADATA_KEY, types, target);\n };\n}\n\n/** The documents this contract feeds — its declaration, else {@link DEFAULT_API_TYPES}. */\n// webpieces-disable no-function-outside-class -- metadata reader paired with the decorator\nexport function getApiTypes(apiClass: Function): readonly ApiTypeKind[] {\n const declared: ApiTypeKind[] | undefined = Reflect.getMetadata(\n API_TYPE_METADATA_KEY,\n apiClass,\n );\n return declared ?? DEFAULT_API_TYPES;\n}\n\n/**\n * MCP membership and `@WpMcpTool` must AGREE, in both directions.\n *\n * A wiring-time assert rather than a type, because the two declarations sit on different decorators\n * at different levels and TypeScript cannot relate them. It is the sibling of\n * `assertEveryEndpointHasAuthMode`, and exists for the same reason.\n */\n// webpieces-disable no-function-outside-class -- wiring-time assert, sibling of assertEveryEndpointHasAuthMode\nexport function assertApiTypeMatchesMcpTools(apiClass: Function): void {\n const declaresMcp = getApiTypes(apiClass).includes(MCP);\n const tools = getWpMcpTools(apiClass);\n const excluded = getInvalidEndpointsForMcp(apiClass);\n assertNoToolIsAlsoExcluded(apiClass, tools, excluded);\n if (declaresMcp && tools.length === 0) {\n // The all-excluded case gets its own sentence because its cure is the opposite one: there is\n // nothing to add, and the contract should stop declaring MCP.\n if (excluded.length > 0 && excluded.length === getApiTypeEndpointNames(apiClass).length) {\n throw new Error(\n `${apiClass.name} declares @ApiType(..., MCP) but EVERY endpoint on it is ` +\n '@InvalidEndpointForMcp. Drop MCP from the @ApiType list — a contract whose ' +\n 'every method is permanently outside MCP does not feed the MCP document.',\n );\n }\n throw new Error(\n `${apiClass.name} declares @ApiType(..., MCP) but no method carries @WpMcpTool. ` +\n \"Add @WpMcpTool('<stable_tool_name>') to the methods agents may call, or drop MCP \" +\n 'from the @ApiType list.',\n );\n }\n if (!declaresMcp && tools.length > 0) {\n const named = tools.map((tool: WpMcpToolMetadata) => tool.methodName).join(', ');\n throw new Error(\n `${apiClass.name} has @WpMcpTool on ${named} but does not declare @ApiType(..., MCP). ` +\n 'Add MCP to the @ApiType list — membership has ONE spelling, so a tool on a ' +\n 'contract nobody published to agents is a contradiction, not a hint.',\n );\n }\n}\n\n/**\n * `@WpMcpTool` and `@InvalidEndpointForMcp` on ONE method is a contradiction, not a precedence rule.\n *\n * Resolving it either way would make the pair a second spelling of a decision that already has one —\n * shim shape #1 in `.claude/rules/no-backwards-compat.md` — and whichever way it resolved, half the\n * readers of that method would be wrong about what it does.\n */\n// webpieces-disable no-function-outside-class -- private assert of assertApiTypeMatchesMcpTools, beside it\nfunction assertNoToolIsAlsoExcluded(\n apiClass: Function,\n tools: readonly WpMcpToolMetadata[],\n excluded: readonly InvalidEndpointForMcpMetadata[],\n): void {\n const excludedNames = new Set(\n excluded.map((one: InvalidEndpointForMcpMetadata) => one.methodName),\n );\n const both = tools\n .filter((tool: WpMcpToolMetadata) => excludedNames.has(tool.methodName))\n .map((tool: WpMcpToolMetadata) => tool.methodName);\n if (both.length === 0) return;\n throw new Error(\n `${apiClass.name} carries BOTH @WpMcpTool and @InvalidEndpointForMcp on ${both.join(', ')}. ` +\n 'They contradict each other — one publishes the method to agents and the other declares ' +\n 'it permanently unusable as a tool. Delete whichever one is wrong.',\n );\n}\n\n/** Reads the endpoints map so a caller can see which methods exist without importing decorators.ts. */\n// webpieces-disable no-function-outside-class -- metadata reader, sibling of getApiTypes\nexport function getApiTypeEndpointNames(apiClass: Function): readonly string[] {\n const endpoints: Record<string, string> =\n Reflect.getMetadata(METADATA_KEYS.ENDPOINTS, apiClass) ?? {};\n return Object.keys(endpoints);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"api-type.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/api-type.ts"],"names":[],"mappings":";;;AA2EA,0BAKC;AAID,kCAMC;AAUD,oEA6BC;AA+BD,0DAIC;AApKD,4BAA0B;AAC1B,6CAA6C;AAC7C,oDAK4B;AAE5B,8EAA8E;AAC9E,IAAK,YAIJ;AAJD,WAAK,YAAY;IACb,yCAAyB,CAAA;IACzB,uDAAuC,CAAA;IACvC,2BAAW,CAAA;AACf,CAAC,EAJI,YAAY,KAAZ,YAAY,QAIhB;AAED,wDAAwD;AAC3C,QAAA,UAAU,GAAG,YAAY,CAAC,UAAU,CAAC;AACrC,QAAA,iBAAiB,GAAG,YAAY,CAAC,iBAAiB,CAAC;AACnD,QAAA,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC;AAKpC,0FAA0F;AAC7E,QAAA,qBAAqB,GAAG,oBAAoB,CAAC;AAE1D,8FAA8F;AACjF,QAAA,iBAAiB,GAA2B,CAAC,kBAAU,CAAC,CAAC;AAEtE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,2GAA2G;AAC3G,SAAgB,OAAO,CAAC,KAAkB,EAAE,GAAG,IAA4B;IACvE,MAAM,KAAK,GAAG,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,CAAC;IAC/B,OAAO,CAAC,MAAgB,EAAE,EAAE;QACxB,OAAO,CAAC,cAAc,CAAC,6BAAqB,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IACjE,CAAC,CAAC;AACN,CAAC;AAED,2FAA2F;AAC3F,2FAA2F;AAC3F,SAAgB,WAAW,CAAC,QAAkB;IAC1C,MAAM,QAAQ,GAA8B,OAAO,CAAC,WAAW,CAC3D,6BAAqB,EACrB,QAAQ,CACX,CAAC;IACF,OAAO,QAAQ,IAAI,yBAAiB,CAAC;AACzC,CAAC;AAED;;;;;;GAMG;AACH,+GAA+G;AAC/G,SAAgB,4BAA4B,CAAC,QAAkB;IAC3D,MAAM,WAAW,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,WAAG,CAAC,CAAC;IACxD,MAAM,KAAK,GAAG,IAAA,2BAAa,EAAC,QAAQ,CAAC,CAAC;IACtC,MAAM,QAAQ,GAAG,IAAA,uCAAyB,EAAC,QAAQ,CAAC,CAAC;IACrD,0BAA0B,CAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;IACtD,IAAI,WAAW,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpC,6FAA6F;QAC7F,8DAA8D;QAC9D,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,MAAM,KAAK,uBAAuB,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC;YACtF,MAAM,IAAI,KAAK,CACX,GAAG,QAAQ,CAAC,IAAI,2DAA2D;gBACvE,6EAA6E;gBAC7E,yEAAyE,CAChF,CAAC;QACN,CAAC;QACD,MAAM,IAAI,KAAK,CACX,GAAG,QAAQ,CAAC,IAAI,iEAAiE;YAC7E,gFAAgF;YAChF,sDAAsD,CAC7D,CAAC;IACN,CAAC;IACD,IAAI,CAAC,WAAW,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAuB,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjF,MAAM,IAAI,KAAK,CACX,GAAG,QAAQ,CAAC,IAAI,sBAAsB,KAAK,4CAA4C;YACnF,6EAA6E;YAC7E,qEAAqE,CAC5E,CAAC;IACN,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,2GAA2G;AAC3G,SAAS,0BAA0B,CAC/B,QAAkB,EAClB,KAAmC,EACnC,QAAkD;IAElD,MAAM,aAAa,GAAG,IAAI,GAAG,CACzB,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAkC,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,CACvE,CAAC;IACF,MAAM,IAAI,GAAG,KAAK;SACb,MAAM,CAAC,CAAC,IAAuB,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;SACvE,GAAG,CAAC,CAAC,IAAuB,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACvD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAC9B,MAAM,IAAI,KAAK,CACX,GAAG,QAAQ,CAAC,IAAI,0DAA0D,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;QACzF,yFAAyF;QACzF,mEAAmE,CAC1E,CAAC;AACN,CAAC;AAED,uGAAuG;AACvG,yFAAyF;AACzF,SAAgB,uBAAuB,CAAC,QAAkB;IACtD,MAAM,SAAS,GACX,OAAO,CAAC,WAAW,CAAC,0BAAa,CAAC,SAAS,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;IACjE,OAAO,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC","sourcesContent":["import 'reflect-metadata';\nimport { METADATA_KEYS } from './decorators';\nimport {\n InvalidEndpointForMcpMetadata,\n getInvalidEndpointsForMcp,\n getWpMcpTools,\n WpMcpToolMetadata,\n} from '../mcp/McpMetadata';\n\n/** Nominal backing keeps raw string literals out of contract declarations. */\nenum ApiTypeValue {\n SVC_TO_SVC = 'svc-to-svc',\n EXTERNAL_CUSTOMER = 'external-customer',\n MCP = 'mcp',\n}\n\n/** Short, statically importable decorator arguments. */\nexport const SVC_TO_SVC = ApiTypeValue.SVC_TO_SVC;\nexport const EXTERNAL_CUSTOMER = ApiTypeValue.EXTERNAL_CUSTOMER;\nexport const MCP = ApiTypeValue.MCP;\n\n/** WHICH generated documents a contract feeds. One value per document. */\nexport type ApiTypeKind = typeof SVC_TO_SVC | typeof EXTERNAL_CUSTOMER | typeof MCP;\n\n/** Metadata key for the contract-level declaration, parallel to {@link METADATA_KEYS}. */\nexport const API_TYPE_METADATA_KEY = 'webpieces:api-type';\n\n/** The fail-closed default: a contract that says nothing feeds only the internal document. */\nexport const DEFAULT_API_TYPES: readonly ApiTypeKind[] = [SVC_TO_SVC];\n\n/**\n * WHICH generated documents this CONTRACT feeds.\n *\n * ```typescript\n * @ApiPath('/stores')\n * @ApiType(SVC_TO_SVC, EXTERNAL_CUSTOMER) // private + customer documents\n * export abstract class StoreApi { ... }\n * ```\n *\n * | value | document |\n * |---|---|\n * | `SVC_TO_SVC` | `full-private-openapi.json` |\n * | `EXTERNAL_CUSTOMER` | `public-openapi.json` |\n * | `MCP` | `mcp-openapi.json` |\n *\n * A document is WRITTEN when at least one contract declares its type, and it contains the contracts\n * that declared it and no others. No contract declaring `MCP` therefore means no `mcp-openapi.json`\n * at all — there is no separate emptiness rule to keep in step with this one.\n *\n * ## Absent means `SVC_TO_SVC` only, and the level is why that is affordable\n *\n * `.claude/rules/no-backwards-compat.md` shim shape #5 rejects \"a widening that is an ABSENCE rather\n * than a token\": a default that publishes to customers would mean you reach them having typed\n * nothing, so the safe state requires an edit and the widest grant is free. Naming the type inverts\n * that — `grep -rn EXTERNAL_CUSTOMER` is the entire customer-facing surface on one screen.\n *\n * It is a CLASS-level declaration because that costs one token per contract rather than one per\n * method. A per-method audience list was the earlier design and was rejected on exactly that\n * ergonomic ground; the per-METHOD case that remains — a new endpoint on an already-published API —\n * is served by `{ hidden: true }` in `EndpointOptions`, which subtracts one method from the customer\n * document and leaves it in the other two. The two are orthogonal: this one selects FILES, that one\n * subtracts a METHOD from one file.\n *\n * ## `MCP` and `@WpMcpTool` are ONE decision\n *\n * `MCP` on a contract with no `@WpMcpTool` method is an error, and a `@WpMcpTool` method on a\n * contract that does not declare `MCP` is an error. Both directions are asserted by\n * {@link assertApiTypeMatchesMcpTools}; without that, membership is declared in two places and they\n * can disagree, which is the defect this whole documentation epic exists to remove.\n *\n * `@InvalidEndpointForMcp` sits OUTSIDE that biconditional: a method declared permanently unusable\n * as a tool is neither required to carry `@WpMcpTool` nor permitted to. A contract on which EVERY\n * endpoint is so declared must not declare `MCP` at all, and that too is asserted there.\n */\n// webpieces-disable no-function-outside-class -- decorator factory; decorators are inherently module-scope\nexport function ApiType(first: ApiTypeKind, ...rest: readonly ApiTypeKind[]): ClassDecorator {\n const types = [first, ...rest];\n return (target: Function) => {\n Reflect.defineMetadata(API_TYPE_METADATA_KEY, types, target);\n };\n}\n\n/** The documents this contract feeds — its declaration, else {@link DEFAULT_API_TYPES}. */\n// webpieces-disable no-function-outside-class -- metadata reader paired with the decorator\nexport function getApiTypes(apiClass: Function): readonly ApiTypeKind[] {\n const declared: ApiTypeKind[] | undefined = Reflect.getMetadata(\n API_TYPE_METADATA_KEY,\n apiClass,\n );\n return declared ?? DEFAULT_API_TYPES;\n}\n\n/**\n * MCP membership and `@WpMcpTool` must AGREE, in both directions.\n *\n * A wiring-time assert rather than a type, because the two declarations sit on different decorators\n * at different levels and TypeScript cannot relate them. It is the sibling of\n * `assertEveryEndpointHasAuthMode`, and exists for the same reason.\n */\n// webpieces-disable no-function-outside-class -- wiring-time assert, sibling of assertEveryEndpointHasAuthMode\nexport function assertApiTypeMatchesMcpTools(apiClass: Function): void {\n const declaresMcp = getApiTypes(apiClass).includes(MCP);\n const tools = getWpMcpTools(apiClass);\n const excluded = getInvalidEndpointsForMcp(apiClass);\n assertNoToolIsAlsoExcluded(apiClass, tools, excluded);\n if (declaresMcp && tools.length === 0) {\n // The all-excluded case gets its own sentence because its cure is the opposite one: there is\n // nothing to add, and the contract should stop declaring MCP.\n if (excluded.length > 0 && excluded.length === getApiTypeEndpointNames(apiClass).length) {\n throw new Error(\n `${apiClass.name} declares @ApiType(..., MCP) but EVERY endpoint on it is ` +\n '@InvalidEndpointForMcp. Drop MCP from the @ApiType list — a contract whose ' +\n 'every method is permanently outside MCP does not feed the MCP document.',\n );\n }\n throw new Error(\n `${apiClass.name} declares @ApiType(..., MCP) but no method carries @WpMcpTool. ` +\n \"Add @WpMcpTool('<stable_tool_name>', '<Human-readable title>') to the methods \" +\n 'agents may call, or drop MCP from the @ApiType list.',\n );\n }\n if (!declaresMcp && tools.length > 0) {\n const named = tools.map((tool: WpMcpToolMetadata) => tool.methodName).join(', ');\n throw new Error(\n `${apiClass.name} has @WpMcpTool on ${named} but does not declare @ApiType(..., MCP). ` +\n 'Add MCP to the @ApiType list — membership has ONE spelling, so a tool on a ' +\n 'contract nobody published to agents is a contradiction, not a hint.',\n );\n }\n}\n\n/**\n * `@WpMcpTool` and `@InvalidEndpointForMcp` on ONE method is a contradiction, not a precedence rule.\n *\n * Resolving it either way would make the pair a second spelling of a decision that already has one —\n * shim shape #1 in `.claude/rules/no-backwards-compat.md` — and whichever way it resolved, half the\n * readers of that method would be wrong about what it does.\n */\n// webpieces-disable no-function-outside-class -- private assert of assertApiTypeMatchesMcpTools, beside it\nfunction assertNoToolIsAlsoExcluded(\n apiClass: Function,\n tools: readonly WpMcpToolMetadata[],\n excluded: readonly InvalidEndpointForMcpMetadata[],\n): void {\n const excludedNames = new Set(\n excluded.map((one: InvalidEndpointForMcpMetadata) => one.methodName),\n );\n const both = tools\n .filter((tool: WpMcpToolMetadata) => excludedNames.has(tool.methodName))\n .map((tool: WpMcpToolMetadata) => tool.methodName);\n if (both.length === 0) return;\n throw new Error(\n `${apiClass.name} carries BOTH @WpMcpTool and @InvalidEndpointForMcp on ${both.join(', ')}. ` +\n 'They contradict each other — one publishes the method to agents and the other declares ' +\n 'it permanently unusable as a tool. Delete whichever one is wrong.',\n );\n}\n\n/** Reads the endpoints map so a caller can see which methods exist without importing decorators.ts. */\n// webpieces-disable no-function-outside-class -- metadata reader, sibling of getApiTypes\nexport function getApiTypeEndpointNames(apiClass: Function): readonly string[] {\n const endpoints: Record<string, string> =\n Reflect.getMetadata(METADATA_KEYS.ENDPOINTS, apiClass) ?? {};\n return Object.keys(endpoints);\n}\n"]}
|
|
@@ -9,10 +9,14 @@ const McpMetadata_1 = require("./McpMetadata");
|
|
|
9
9
|
* with TS2578 the moment one of these starts compiling.
|
|
10
10
|
*/
|
|
11
11
|
class InvalidMcpContract {
|
|
12
|
-
// @ts-expect-error @WpMcpTool takes the stable protocol NAME, not an options object
|
|
12
|
+
// @ts-expect-error @WpMcpTool takes the stable protocol NAME and a TITLE, not an options object
|
|
13
13
|
options(_request) {
|
|
14
14
|
throw new Error('contract only');
|
|
15
15
|
}
|
|
16
|
+
// @ts-expect-error the title is REQUIRED (#1180) — the one-argument spelling is gone, not optional
|
|
17
|
+
untitled(_request) {
|
|
18
|
+
throw new Error('contract only');
|
|
19
|
+
}
|
|
16
20
|
// @ts-expect-error side-effect claims belong on @Endpoint.operation, never @WpMcpTool
|
|
17
21
|
hints(_request) {
|
|
18
22
|
throw new Error('contract only');
|
|
@@ -25,7 +29,13 @@ tslib_1.__decorate([
|
|
|
25
29
|
tslib_1.__metadata("design:returntype", Promise)
|
|
26
30
|
], InvalidMcpContract.prototype, "options", null);
|
|
27
31
|
tslib_1.__decorate([
|
|
28
|
-
(0, McpMetadata_1.WpMcpTool)('
|
|
32
|
+
(0, McpMetadata_1.WpMcpTool)('missing_title'),
|
|
33
|
+
tslib_1.__metadata("design:type", Function),
|
|
34
|
+
tslib_1.__metadata("design:paramtypes", [Object]),
|
|
35
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
36
|
+
], InvalidMcpContract.prototype, "untitled", null);
|
|
37
|
+
tslib_1.__decorate([
|
|
38
|
+
(0, McpMetadata_1.WpMcpTool)('invalid', 'Invalid tool', true),
|
|
29
39
|
tslib_1.__metadata("design:type", Function),
|
|
30
40
|
tslib_1.__metadata("design:paramtypes", [Object]),
|
|
31
41
|
tslib_1.__metadata("design:returntype", Promise)
|
|
@@ -41,13 +51,13 @@ class InvalidMcpArities {
|
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
53
|
tslib_1.__decorate([
|
|
44
|
-
(0, McpMetadata_1.WpMcpTool)('zero-arity'),
|
|
54
|
+
(0, McpMetadata_1.WpMcpTool)('zero-arity', 'Zero arity'),
|
|
45
55
|
tslib_1.__metadata("design:type", Function),
|
|
46
56
|
tslib_1.__metadata("design:paramtypes", []),
|
|
47
57
|
tslib_1.__metadata("design:returntype", Promise)
|
|
48
58
|
], InvalidMcpArities.prototype, "zero", null);
|
|
49
59
|
tslib_1.__decorate([
|
|
50
|
-
(0, McpMetadata_1.WpMcpTool)('multi-arity'),
|
|
60
|
+
(0, McpMetadata_1.WpMcpTool)('multi-arity', 'Multi arity'),
|
|
51
61
|
tslib_1.__metadata("design:type", Function),
|
|
52
62
|
tslib_1.__metadata("design:paramtypes", [Object, Object]),
|
|
53
63
|
tslib_1.__metadata("design:returntype", Promise)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/mcp/McpCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,+CAA0C;AAE1C;;;;;GAKG;AACH,MAAe,kBAAkB;IAC7B,
|
|
1
|
+
{"version":3,"file":"McpCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/mcp/McpCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,+CAA0C;AAE1C;;;;;GAKG;AACH,MAAe,kBAAkB;IAC7B,gGAAgG;IAEhG,OAAO,CAAC,QAAgB;QACpB,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;IACrC,CAAC;IAED,mGAAmG;IAEnG,QAAQ,CAAC,QAAgB;QACrB,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;IACrC,CAAC;IAED,sFAAsF;IAEtF,KAAK,CAAC,QAAgB;QAClB,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;IACrC,CAAC;CACJ;AAfG;IADC,IAAA,uBAAS,EAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;;;iDAG9B;AAID;IADC,IAAA,uBAAS,EAAC,eAAe,CAAC;;;;kDAG1B;AAID;IADC,IAAA,uBAAS,EAAC,SAAS,EAAE,cAAc,EAAE,IAAI,CAAC;;;;+CAG1C;AAGL,MAAe,iBAAiB;IAC5B,6DAA6D;IAE7D,IAAI;QACA,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;IACrC,CAAC;IAED,6DAA6D;IAE7D,KAAK,CAAC,MAAc,EAAE,OAAe;QACjC,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;IACrC,CAAC;CACJ;AATG;IADC,IAAA,uBAAS,EAAC,YAAY,EAAE,YAAY,CAAC;;;;6CAGrC;AAID;IADC,IAAA,uBAAS,EAAC,aAAa,EAAE,aAAa,CAAC;;;;8CAGvC;AAGL,KAAK,kBAAkB,CAAC;AACxB,KAAK,iBAAiB,CAAC","sourcesContent":["import { WpMcpTool } from './McpMetadata';\n\n/**\n * Compile-time pins for `@WpMcpTool`. These live in a COMPILED file rather than a `.spec.ts` because\n * vitest strips types with esbuild and `tsconfig.lib.json` excludes specs, so a `@ts-expect-error` in\n * a spec is inert (`.claude/rules/no-backwards-compat.md` shim shape #4). Here, tsc fails the build\n * with TS2578 the moment one of these starts compiling.\n */\nabstract class InvalidMcpContract {\n // @ts-expect-error @WpMcpTool takes the stable protocol NAME and a TITLE, not an options object\n @WpMcpTool({ name: 'invalid' })\n options(_request: object): Promise<object> {\n throw new Error('contract only');\n }\n\n // @ts-expect-error the title is REQUIRED (#1180) — the one-argument spelling is gone, not optional\n @WpMcpTool('missing_title')\n untitled(_request: object): Promise<object> {\n throw new Error('contract only');\n }\n\n // @ts-expect-error side-effect claims belong on @Endpoint.operation, never @WpMcpTool\n @WpMcpTool('invalid', 'Invalid tool', true)\n hints(_request: object): Promise<object> {\n throw new Error('contract only');\n }\n}\n\nabstract class InvalidMcpArities {\n // @ts-expect-error MCP tools require exactly one request DTO\n @WpMcpTool('zero-arity', 'Zero arity')\n zero(): Promise<object> {\n throw new Error('contract only');\n }\n\n // @ts-expect-error MCP tools require exactly one request DTO\n @WpMcpTool('multi-arity', 'Multi arity')\n multi(_first: object, _second: object): Promise<object> {\n throw new Error('contract only');\n }\n}\n\nvoid InvalidMcpContract;\nvoid InvalidMcpArities;\n"]}
|
package/src/mcp/McpMetadata.d.ts
CHANGED
|
@@ -17,33 +17,45 @@ export type WpMcpMethodDecorator = <TMethod extends AsyncObjectMethod>(target: o
|
|
|
17
17
|
export declare class WpMcpToolMetadata {
|
|
18
18
|
readonly methodName: string;
|
|
19
19
|
readonly name: string;
|
|
20
|
-
|
|
20
|
+
/** The human-readable display name; `tools/list` publishes it as `title` AND `annotations.title`. */
|
|
21
|
+
readonly title: string;
|
|
22
|
+
constructor(methodName: string, name: string,
|
|
23
|
+
/** The human-readable display name; `tools/list` publishes it as `title` AND `annotations.title`. */
|
|
24
|
+
title: string);
|
|
21
25
|
}
|
|
22
26
|
/**
|
|
23
27
|
* Explicitly publishes an existing `@Endpoint(POST, path, WRITE, RPC)` method as an MCP tool, under
|
|
24
|
-
* the STABLE protocol name given here.
|
|
28
|
+
* the STABLE protocol name and the human-readable TITLE given here.
|
|
25
29
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
30
|
+
* ```typescript
|
|
31
|
+
* @WpMcpTool('learner_get_passages', 'Get your passages')
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* Those two are the whole argument list — the only facts about a tool the source cannot otherwise
|
|
35
|
+
* state:
|
|
29
36
|
*
|
|
30
37
|
* | fact | where it comes from |
|
|
31
38
|
* |---|---|
|
|
39
|
+
* | `name` | this decorator — the stable protocol identity, independent of the method name because renaming a method must not break a saved agent workflow |
|
|
40
|
+
* | `title` | this decorator — the display name clients and the Claude Connectors Directory show; published as BOTH `Tool.title` and `annotations.title` |
|
|
32
41
|
* | `description` | the method's JSDoc body, or its `@mcp` tag — the same words the partner reads |
|
|
33
42
|
* | `readOnlyHint` / `destructiveHint` / `idempotentHint` | `@Endpoint`'s `operation`, via {@link mcpHintsForOperation} |
|
|
34
43
|
* | `openWorldHint` | `@Endpoint`'s `openWorld` option |
|
|
35
44
|
* | `inputSchema` / `outputSchema` | the declared request and response types |
|
|
36
45
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
46
|
+
* ## Why the title is REQUIRED and never derived (#1180)
|
|
47
|
+
*
|
|
48
|
+
* The Connectors Directory flags every tool without `annotations.title`. An optional title gets
|
|
49
|
+
* skipped and the miss surfaces only at directory-submission time, so it is a compile-time
|
|
50
|
+
* requirement and an empty or whitespace one throws at decoration time. It is never computed from
|
|
51
|
+
* the name: `learner_get_passages` → "Learner get passages" is exactly the mechanical listing text
|
|
52
|
+
* reviewers push back on, and the area prefix means nothing to an end user.
|
|
40
53
|
*
|
|
41
|
-
* Until #984
|
|
42
|
-
* paragraph drift the first time somebody edits one,
|
|
43
|
-
*
|
|
44
|
-
* held one field, so it is a plain string and `WpMcpToolOptions` is deleted with it.
|
|
54
|
+
* Until #984 the decorator also took a `description`, duplicating the JSDoc. Two authored copies of
|
|
55
|
+
* one paragraph drift the first time somebody edits one, so it is gone; the title is a short NAME,
|
|
56
|
+
* not a second copy of the description.
|
|
45
57
|
*/
|
|
46
|
-
export declare function WpMcpTool(name: string): WpMcpMethodDecorator;
|
|
58
|
+
export declare function WpMcpTool(name: string, title: string): WpMcpMethodDecorator;
|
|
47
59
|
/** Runtime metadata for one endpoint declared PERMANENTLY outside MCP. */
|
|
48
60
|
export declare class InvalidEndpointForMcpMetadata {
|
|
49
61
|
readonly methodName: string;
|
package/src/mcp/McpMetadata.js
CHANGED
|
@@ -27,45 +27,62 @@ exports.WpMcpToolHints = WpMcpToolHints;
|
|
|
27
27
|
class WpMcpToolMetadata {
|
|
28
28
|
methodName;
|
|
29
29
|
name;
|
|
30
|
-
|
|
30
|
+
title;
|
|
31
|
+
constructor(methodName, name,
|
|
32
|
+
/** The human-readable display name; `tools/list` publishes it as `title` AND `annotations.title`. */
|
|
33
|
+
title) {
|
|
31
34
|
this.methodName = methodName;
|
|
32
35
|
this.name = name;
|
|
36
|
+
this.title = title;
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
exports.WpMcpToolMetadata = WpMcpToolMetadata;
|
|
36
40
|
/**
|
|
37
41
|
* Explicitly publishes an existing `@Endpoint(POST, path, WRITE, RPC)` method as an MCP tool, under
|
|
38
|
-
* the STABLE protocol name given here.
|
|
42
|
+
* the STABLE protocol name and the human-readable TITLE given here.
|
|
39
43
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
44
|
+
* ```typescript
|
|
45
|
+
* @WpMcpTool('learner_get_passages', 'Get your passages')
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* Those two are the whole argument list — the only facts about a tool the source cannot otherwise
|
|
49
|
+
* state:
|
|
43
50
|
*
|
|
44
51
|
* | fact | where it comes from |
|
|
45
52
|
* |---|---|
|
|
53
|
+
* | `name` | this decorator — the stable protocol identity, independent of the method name because renaming a method must not break a saved agent workflow |
|
|
54
|
+
* | `title` | this decorator — the display name clients and the Claude Connectors Directory show; published as BOTH `Tool.title` and `annotations.title` |
|
|
46
55
|
* | `description` | the method's JSDoc body, or its `@mcp` tag — the same words the partner reads |
|
|
47
56
|
* | `readOnlyHint` / `destructiveHint` / `idempotentHint` | `@Endpoint`'s `operation`, via {@link mcpHintsForOperation} |
|
|
48
57
|
* | `openWorldHint` | `@Endpoint`'s `openWorld` option |
|
|
49
58
|
* | `inputSchema` / `outputSchema` | the declared request and response types |
|
|
50
59
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
60
|
+
* ## Why the title is REQUIRED and never derived (#1180)
|
|
61
|
+
*
|
|
62
|
+
* The Connectors Directory flags every tool without `annotations.title`. An optional title gets
|
|
63
|
+
* skipped and the miss surfaces only at directory-submission time, so it is a compile-time
|
|
64
|
+
* requirement and an empty or whitespace one throws at decoration time. It is never computed from
|
|
65
|
+
* the name: `learner_get_passages` → "Learner get passages" is exactly the mechanical listing text
|
|
66
|
+
* reviewers push back on, and the area prefix means nothing to an end user.
|
|
54
67
|
*
|
|
55
|
-
* Until #984
|
|
56
|
-
* paragraph drift the first time somebody edits one,
|
|
57
|
-
*
|
|
58
|
-
* held one field, so it is a plain string and `WpMcpToolOptions` is deleted with it.
|
|
68
|
+
* Until #984 the decorator also took a `description`, duplicating the JSDoc. Two authored copies of
|
|
69
|
+
* one paragraph drift the first time somebody edits one, so it is gone; the title is a short NAME,
|
|
70
|
+
* not a second copy of the description.
|
|
59
71
|
*/
|
|
60
72
|
// webpieces-disable no-function-outside-class -- decorator factories are inherently module-scope
|
|
61
|
-
function WpMcpTool(name) {
|
|
73
|
+
function WpMcpTool(name, title) {
|
|
62
74
|
if (name.trim() === '')
|
|
63
75
|
throw new Error('@WpMcpTool requires a non-empty stable name.');
|
|
76
|
+
if (title.trim() === '') {
|
|
77
|
+
throw new Error(`@WpMcpTool('${name}', ...) requires a non-empty human-readable title, e.g. ` +
|
|
78
|
+
"@WpMcpTool('learner_get_passages', 'Get your passages'). tools/list publishes it as " +
|
|
79
|
+
'title and annotations.title, and the Claude Connectors Directory flags a tool without one.');
|
|
80
|
+
}
|
|
64
81
|
return (target, propertyKey, _descriptor) => {
|
|
65
82
|
const apiClass = target.constructor;
|
|
66
83
|
const tools = Reflect.getMetadata(decorators_1.METADATA_KEYS.MCP_TOOLS, apiClass) ?? {};
|
|
67
84
|
const methodName = String(propertyKey);
|
|
68
|
-
tools[methodName] = new WpMcpToolMetadata(methodName, name);
|
|
85
|
+
tools[methodName] = new WpMcpToolMetadata(methodName, name, title);
|
|
69
86
|
Reflect.defineMetadata(decorators_1.METADATA_KEYS.MCP_TOOLS, tools, apiClass);
|
|
70
87
|
};
|
|
71
88
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpMetadata.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/mcp/McpMetadata.ts"],"names":[],"mappings":"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","sourcesContent":["import 'reflect-metadata';\nimport { METADATA_KEYS } from '../http/decorators';\nimport { EndpointOperation, READ, WRITE_IDEMPOTENT, WRITE } from '../http/HttpEndpointOptions';\n\n/** The immutable hints MCP clients use when deciding whether and how to call a tool. */\nexport class WpMcpToolHints {\n constructor(\n public readonly readOnlyHint: boolean,\n public readonly destructiveHint: boolean,\n public readonly idempotentHint: boolean,\n public readonly openWorldHint: boolean,\n ) {}\n}\n\ntype AsyncObjectMethod = (...args: never[]) => Promise<object>;\ntype ExactOneParameter<TMethod extends AsyncObjectMethod> = Parameters<TMethod>['length'] extends 1\n ? object\n : never;\ntype ExactOneMethodDescriptor<TMethod extends AsyncObjectMethod> =\n TypedPropertyDescriptor<TMethod> & ExactOneParameter<TMethod>;\n\n/** Method decorator restricted to one request DTO and one async response DTO. */\nexport type WpMcpMethodDecorator = <TMethod extends AsyncObjectMethod>(\n target: object,\n propertyKey: string | symbol,\n descriptor: ExactOneMethodDescriptor<TMethod>,\n) => void;\n\n/** Runtime metadata for one explicitly exposed endpoint. */\nexport class WpMcpToolMetadata {\n constructor(\n public readonly methodName: string,\n public readonly name: string,\n ) {}\n}\n\n/**\n * Explicitly publishes an existing `@Endpoint(POST, path, WRITE, RPC)` method as an MCP tool, under\n * the STABLE protocol name given here.\n *\n * The name is the whole argument, and deliberately the only one. It is independent of the method\n * name because renaming a method must not break a saved agent workflow, and nothing else about a\n * tool is unsayable in the source:\n *\n * | fact | where it comes from |\n * |---|---|\n * | `description` | the method's JSDoc body, or its `@mcp` tag — the same words the partner reads |\n * | `readOnlyHint` / `destructiveHint` / `idempotentHint` | `@Endpoint`'s `operation`, via {@link mcpHintsForOperation} |\n * | `openWorldHint` | `@Endpoint`'s `openWorld` option |\n * | `inputSchema` / `outputSchema` | the declared request and response types |\n *\n * ```typescript\n * @WpMcpTool('search_stores')\n * ```\n *\n * Until #984 it also took a `description`, duplicating the JSDoc. Two authored copies of one\n * paragraph drift the first time somebody edits one, and nothing catches it — the partner reads the\n * OpenAPI text and the agent reads the decorator text. With `description` gone the options object\n * held one field, so it is a plain string and `WpMcpToolOptions` is deleted with it.\n */\n// webpieces-disable no-function-outside-class -- decorator factories are inherently module-scope\nexport function WpMcpTool(name: string): WpMcpMethodDecorator {\n if (name.trim() === '') throw new Error('@WpMcpTool requires a non-empty stable name.');\n return <TMethod extends AsyncObjectMethod>(\n target: object,\n propertyKey: string | symbol,\n _descriptor: ExactOneMethodDescriptor<TMethod>,\n ): void => {\n const apiClass = target.constructor;\n const tools: Record<string, WpMcpToolMetadata> =\n Reflect.getMetadata(METADATA_KEYS.MCP_TOOLS, apiClass) ?? {};\n const methodName = String(propertyKey);\n tools[methodName] = new WpMcpToolMetadata(methodName, name);\n Reflect.defineMetadata(METADATA_KEYS.MCP_TOOLS, tools, apiClass);\n };\n}\n\n/** Runtime metadata for one endpoint declared PERMANENTLY outside MCP. */\nexport class InvalidEndpointForMcpMetadata {\n constructor(\n public readonly methodName: string,\n /** WHY it can never be a tool. Non-empty, enforced at decoration time. */\n public readonly reason: string,\n ) {}\n}\n\n/**\n * Declares that this endpoint is PERMANENTLY outside MCP, and says why.\n *\n * ## It is not a suppression, and that distinction is the whole feature\n *\n * `// webpieces-disable api-rules-for-mcp -- <reason>` says \"stop complaining\". This says \"this is\n * structurally impossible, forever\", and the two have different futures: a suppression is something\n * somebody should eventually come back to, and this is something nobody should. Both measured cases\n * are types that are CORRECT and must not be \"fixed\" —\n *\n * - a webhook TRANSPORT envelope, whose `data` is owned by each event's own published contract;\n * typing it here would fork that ownership and make adding an event type a change to the\n * transport contract rather than a registration, and\n * - a freeform-SQL escape hatch, whose result columns depend on the model-authored `SELECT`.\n *\n * The second one is worth stating rather than burying: it is the most obviously useful tool in that\n * repo, and it is excluded by a type that is right.\n *\n * ## Absence already says \"not a tool YET\"\n *\n * A method with no `@WpMcpTool` is simply not published. This decorator is the NEVER, and the reason\n * is what makes `grep -rn InvalidEndpointForMcp` a list of permanent exclusions with their arguments\n * rather than a list of names. `api-rules-for-mcp` restates that list on every run, so an exclusion\n * stays visible instead of being announced once and forgotten.\n *\n * ## It contradicts `@WpMcpTool`\n *\n * Both on one method is an ERROR, asserted by {@link assertApiTypeMatchesMcpTools}. \"Publish this as\n * a tool\" and \"this can never be a tool\" are two spellings of one decision, which is exactly the\n * defect `.claude/rules/no-backwards-compat.md` shim shape #1 rejects.\n *\n * ```typescript\n * @InvalidEndpointForMcp('a transport envelope body is opaque by design; the published partner ' +\n * 'contract for each event type owns its shape')\n * ```\n */\n// webpieces-disable no-function-outside-class -- decorator factories are inherently module-scope\nexport function InvalidEndpointForMcp(reason: string): MethodDecorator {\n if (reason.trim() === '') {\n throw new Error(\n '@InvalidEndpointForMcp requires a non-empty reason. The reason IS the declaration — ' +\n 'without it this is a suppression with a longer name.',\n );\n }\n return (target: object, propertyKey: string | symbol): void => {\n const apiClass = target.constructor;\n const excluded: Record<string, InvalidEndpointForMcpMetadata> =\n Reflect.getMetadata(METADATA_KEYS.MCP_INVALID, apiClass) ?? {};\n const methodName = String(propertyKey);\n excluded[methodName] = new InvalidEndpointForMcpMetadata(methodName, reason);\n Reflect.defineMetadata(METADATA_KEYS.MCP_INVALID, excluded, apiClass);\n };\n}\n\n/** Every method declared permanently outside MCP by {@link InvalidEndpointForMcp}. */\n// webpieces-disable no-function-outside-class -- metadata reader paired with the decorator\nexport function getInvalidEndpointsForMcp(apiClass: Function): InvalidEndpointForMcpMetadata[] {\n const excluded: Record<string, InvalidEndpointForMcpMetadata> =\n Reflect.getMetadata(METADATA_KEYS.MCP_INVALID, apiClass) ?? {};\n return Object.values(excluded);\n}\n\n/** The one mapping from endpoint side effects to MCP advisory annotations. */\n// webpieces-disable no-function-outside-class -- pure metadata mapping shared by MCP registration\nexport function mcpHintsForOperation(\n operation: EndpointOperation,\n openWorldHint: boolean,\n): WpMcpToolHints {\n switch (operation) {\n case READ:\n return new WpMcpToolHints(true, false, true, openWorldHint);\n case WRITE_IDEMPOTENT:\n return new WpMcpToolHints(false, true, true, openWorldHint);\n case WRITE:\n return new WpMcpToolHints(false, true, false, openWorldHint);\n }\n}\n\n/** Returns only methods explicitly opted in through {@link WpMcpTool}. */\n// webpieces-disable no-function-outside-class -- metadata reader paired with the decorator\nexport function getWpMcpTools(apiClass: Function): WpMcpToolMetadata[] {\n const tools: Record<string, WpMcpToolMetadata> =\n Reflect.getMetadata(METADATA_KEYS.MCP_TOOLS, apiClass) ?? {};\n return Object.values(tools);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"McpMetadata.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/mcp/McpMetadata.ts"],"names":[],"mappings":";;;AAuEA,8BAqBC;AAgDD,sDAeC;AAID,8DAIC;AAID,oDAYC;AAID,sCAIC;AA3LD,4BAA0B;AAC1B,mDAAmD;AACnD,qEAA+F;AAE/F,wFAAwF;AACxF,MAAa,cAAc;IAEH;IACA;IACA;IACA;IAJpB,YACoB,YAAqB,EACrB,eAAwB,EACxB,cAAuB,EACvB,aAAsB;QAHtB,iBAAY,GAAZ,YAAY,CAAS;QACrB,oBAAe,GAAf,eAAe,CAAS;QACxB,mBAAc,GAAd,cAAc,CAAS;QACvB,kBAAa,GAAb,aAAa,CAAS;IACvC,CAAC;CACP;AAPD,wCAOC;AAgBD,4DAA4D;AAC5D,MAAa,iBAAiB;IAEN;IACA;IAEA;IAJpB,YACoB,UAAkB,EAClB,IAAY;IAC5B,qGAAqG;IACrF,KAAa;QAHb,eAAU,GAAV,UAAU,CAAQ;QAClB,SAAI,GAAJ,IAAI,CAAQ;QAEZ,UAAK,GAAL,KAAK,CAAQ;IAC9B,CAAC;CACP;AAPD,8CAOC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,iGAAiG;AACjG,SAAgB,SAAS,CAAC,IAAY,EAAE,KAAa;IACjD,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IACxF,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACX,eAAe,IAAI,0DAA0D;YACzE,sFAAsF;YACtF,4FAA4F,CACnG,CAAC;IACN,CAAC;IACD,OAAO,CACH,MAAc,EACd,WAA4B,EAC5B,WAA8C,EAC1C,EAAE;QACN,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC;QACpC,MAAM,KAAK,GACP,OAAO,CAAC,WAAW,CAAC,0BAAa,CAAC,SAAS,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;QACjE,MAAM,UAAU,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;QACvC,KAAK,CAAC,UAAU,CAAC,GAAG,IAAI,iBAAiB,CAAC,UAAU,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACnE,OAAO,CAAC,cAAc,CAAC,0BAAa,CAAC,SAAS,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;IACrE,CAAC,CAAC;AACN,CAAC;AAED,0EAA0E;AAC1E,MAAa,6BAA6B;IAElB;IAEA;IAHpB,YACoB,UAAkB;IAClC,0EAA0E;IAC1D,MAAc;QAFd,eAAU,GAAV,UAAU,CAAQ;QAElB,WAAM,GAAN,MAAM,CAAQ;IAC/B,CAAC;CACP;AAND,sEAMC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,iGAAiG;AACjG,SAAgB,qBAAqB,CAAC,MAAc;IAChD,IAAI,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACX,sFAAsF;YAClF,sDAAsD,CAC7D,CAAC;IACN,CAAC;IACD,OAAO,CAAC,MAAc,EAAE,WAA4B,EAAQ,EAAE;QAC1D,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC;QACpC,MAAM,QAAQ,GACV,OAAO,CAAC,WAAW,CAAC,0BAAa,CAAC,WAAW,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnE,MAAM,UAAU,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;QACvC,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,6BAA6B,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAC7E,OAAO,CAAC,cAAc,CAAC,0BAAa,CAAC,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC1E,CAAC,CAAC;AACN,CAAC;AAED,sFAAsF;AACtF,2FAA2F;AAC3F,SAAgB,yBAAyB,CAAC,QAAkB;IACxD,MAAM,QAAQ,GACV,OAAO,CAAC,WAAW,CAAC,0BAAa,CAAC,WAAW,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnE,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AACnC,CAAC;AAED,8EAA8E;AAC9E,kGAAkG;AAClG,SAAgB,oBAAoB,CAChC,SAA4B,EAC5B,aAAsB;IAEtB,QAAQ,SAAS,EAAE,CAAC;QAChB,KAAK,0BAAI;YACL,OAAO,IAAI,cAAc,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;QAChE,KAAK,sCAAgB;YACjB,OAAO,IAAI,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;QAChE,KAAK,2BAAK;YACN,OAAO,IAAI,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC;IACrE,CAAC;AACL,CAAC;AAED,0EAA0E;AAC1E,2FAA2F;AAC3F,SAAgB,aAAa,CAAC,QAAkB;IAC5C,MAAM,KAAK,GACP,OAAO,CAAC,WAAW,CAAC,0BAAa,CAAC,SAAS,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;IACjE,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC","sourcesContent":["import 'reflect-metadata';\nimport { METADATA_KEYS } from '../http/decorators';\nimport { EndpointOperation, READ, WRITE_IDEMPOTENT, WRITE } from '../http/HttpEndpointOptions';\n\n/** The immutable hints MCP clients use when deciding whether and how to call a tool. */\nexport class WpMcpToolHints {\n constructor(\n public readonly readOnlyHint: boolean,\n public readonly destructiveHint: boolean,\n public readonly idempotentHint: boolean,\n public readonly openWorldHint: boolean,\n ) {}\n}\n\ntype AsyncObjectMethod = (...args: never[]) => Promise<object>;\ntype ExactOneParameter<TMethod extends AsyncObjectMethod> = Parameters<TMethod>['length'] extends 1\n ? object\n : never;\ntype ExactOneMethodDescriptor<TMethod extends AsyncObjectMethod> =\n TypedPropertyDescriptor<TMethod> & ExactOneParameter<TMethod>;\n\n/** Method decorator restricted to one request DTO and one async response DTO. */\nexport type WpMcpMethodDecorator = <TMethod extends AsyncObjectMethod>(\n target: object,\n propertyKey: string | symbol,\n descriptor: ExactOneMethodDescriptor<TMethod>,\n) => void;\n\n/** Runtime metadata for one explicitly exposed endpoint. */\nexport class WpMcpToolMetadata {\n constructor(\n public readonly methodName: string,\n public readonly name: string,\n /** The human-readable display name; `tools/list` publishes it as `title` AND `annotations.title`. */\n public readonly title: string,\n ) {}\n}\n\n/**\n * Explicitly publishes an existing `@Endpoint(POST, path, WRITE, RPC)` method as an MCP tool, under\n * the STABLE protocol name and the human-readable TITLE given here.\n *\n * ```typescript\n * @WpMcpTool('learner_get_passages', 'Get your passages')\n * ```\n *\n * Those two are the whole argument list — the only facts about a tool the source cannot otherwise\n * state:\n *\n * | fact | where it comes from |\n * |---|---|\n * | `name` | this decorator — the stable protocol identity, independent of the method name because renaming a method must not break a saved agent workflow |\n * | `title` | this decorator — the display name clients and the Claude Connectors Directory show; published as BOTH `Tool.title` and `annotations.title` |\n * | `description` | the method's JSDoc body, or its `@mcp` tag — the same words the partner reads |\n * | `readOnlyHint` / `destructiveHint` / `idempotentHint` | `@Endpoint`'s `operation`, via {@link mcpHintsForOperation} |\n * | `openWorldHint` | `@Endpoint`'s `openWorld` option |\n * | `inputSchema` / `outputSchema` | the declared request and response types |\n *\n * ## Why the title is REQUIRED and never derived (#1180)\n *\n * The Connectors Directory flags every tool without `annotations.title`. An optional title gets\n * skipped and the miss surfaces only at directory-submission time, so it is a compile-time\n * requirement and an empty or whitespace one throws at decoration time. It is never computed from\n * the name: `learner_get_passages` → \"Learner get passages\" is exactly the mechanical listing text\n * reviewers push back on, and the area prefix means nothing to an end user.\n *\n * Until #984 the decorator also took a `description`, duplicating the JSDoc. Two authored copies of\n * one paragraph drift the first time somebody edits one, so it is gone; the title is a short NAME,\n * not a second copy of the description.\n */\n// webpieces-disable no-function-outside-class -- decorator factories are inherently module-scope\nexport function WpMcpTool(name: string, title: string): WpMcpMethodDecorator {\n if (name.trim() === '') throw new Error('@WpMcpTool requires a non-empty stable name.');\n if (title.trim() === '') {\n throw new Error(\n `@WpMcpTool('${name}', ...) requires a non-empty human-readable title, e.g. ` +\n \"@WpMcpTool('learner_get_passages', 'Get your passages'). tools/list publishes it as \" +\n 'title and annotations.title, and the Claude Connectors Directory flags a tool without one.',\n );\n }\n return <TMethod extends AsyncObjectMethod>(\n target: object,\n propertyKey: string | symbol,\n _descriptor: ExactOneMethodDescriptor<TMethod>,\n ): void => {\n const apiClass = target.constructor;\n const tools: Record<string, WpMcpToolMetadata> =\n Reflect.getMetadata(METADATA_KEYS.MCP_TOOLS, apiClass) ?? {};\n const methodName = String(propertyKey);\n tools[methodName] = new WpMcpToolMetadata(methodName, name, title);\n Reflect.defineMetadata(METADATA_KEYS.MCP_TOOLS, tools, apiClass);\n };\n}\n\n/** Runtime metadata for one endpoint declared PERMANENTLY outside MCP. */\nexport class InvalidEndpointForMcpMetadata {\n constructor(\n public readonly methodName: string,\n /** WHY it can never be a tool. Non-empty, enforced at decoration time. */\n public readonly reason: string,\n ) {}\n}\n\n/**\n * Declares that this endpoint is PERMANENTLY outside MCP, and says why.\n *\n * ## It is not a suppression, and that distinction is the whole feature\n *\n * `// webpieces-disable api-rules-for-mcp -- <reason>` says \"stop complaining\". This says \"this is\n * structurally impossible, forever\", and the two have different futures: a suppression is something\n * somebody should eventually come back to, and this is something nobody should. Both measured cases\n * are types that are CORRECT and must not be \"fixed\" —\n *\n * - a webhook TRANSPORT envelope, whose `data` is owned by each event's own published contract;\n * typing it here would fork that ownership and make adding an event type a change to the\n * transport contract rather than a registration, and\n * - a freeform-SQL escape hatch, whose result columns depend on the model-authored `SELECT`.\n *\n * The second one is worth stating rather than burying: it is the most obviously useful tool in that\n * repo, and it is excluded by a type that is right.\n *\n * ## Absence already says \"not a tool YET\"\n *\n * A method with no `@WpMcpTool` is simply not published. This decorator is the NEVER, and the reason\n * is what makes `grep -rn InvalidEndpointForMcp` a list of permanent exclusions with their arguments\n * rather than a list of names. `api-rules-for-mcp` restates that list on every run, so an exclusion\n * stays visible instead of being announced once and forgotten.\n *\n * ## It contradicts `@WpMcpTool`\n *\n * Both on one method is an ERROR, asserted by {@link assertApiTypeMatchesMcpTools}. \"Publish this as\n * a tool\" and \"this can never be a tool\" are two spellings of one decision, which is exactly the\n * defect `.claude/rules/no-backwards-compat.md` shim shape #1 rejects.\n *\n * ```typescript\n * @InvalidEndpointForMcp('a transport envelope body is opaque by design; the published partner ' +\n * 'contract for each event type owns its shape')\n * ```\n */\n// webpieces-disable no-function-outside-class -- decorator factories are inherently module-scope\nexport function InvalidEndpointForMcp(reason: string): MethodDecorator {\n if (reason.trim() === '') {\n throw new Error(\n '@InvalidEndpointForMcp requires a non-empty reason. The reason IS the declaration — ' +\n 'without it this is a suppression with a longer name.',\n );\n }\n return (target: object, propertyKey: string | symbol): void => {\n const apiClass = target.constructor;\n const excluded: Record<string, InvalidEndpointForMcpMetadata> =\n Reflect.getMetadata(METADATA_KEYS.MCP_INVALID, apiClass) ?? {};\n const methodName = String(propertyKey);\n excluded[methodName] = new InvalidEndpointForMcpMetadata(methodName, reason);\n Reflect.defineMetadata(METADATA_KEYS.MCP_INVALID, excluded, apiClass);\n };\n}\n\n/** Every method declared permanently outside MCP by {@link InvalidEndpointForMcp}. */\n// webpieces-disable no-function-outside-class -- metadata reader paired with the decorator\nexport function getInvalidEndpointsForMcp(apiClass: Function): InvalidEndpointForMcpMetadata[] {\n const excluded: Record<string, InvalidEndpointForMcpMetadata> =\n Reflect.getMetadata(METADATA_KEYS.MCP_INVALID, apiClass) ?? {};\n return Object.values(excluded);\n}\n\n/** The one mapping from endpoint side effects to MCP advisory annotations. */\n// webpieces-disable no-function-outside-class -- pure metadata mapping shared by MCP registration\nexport function mcpHintsForOperation(\n operation: EndpointOperation,\n openWorldHint: boolean,\n): WpMcpToolHints {\n switch (operation) {\n case READ:\n return new WpMcpToolHints(true, false, true, openWorldHint);\n case WRITE_IDEMPOTENT:\n return new WpMcpToolHints(false, true, true, openWorldHint);\n case WRITE:\n return new WpMcpToolHints(false, true, false, openWorldHint);\n }\n}\n\n/** Returns only methods explicitly opted in through {@link WpMcpTool}. */\n// webpieces-disable no-function-outside-class -- metadata reader paired with the decorator\nexport function getWpMcpTools(apiClass: Function): WpMcpToolMetadata[] {\n const tools: Record<string, WpMcpToolMetadata> =\n Reflect.getMetadata(METADATA_KEYS.MCP_TOOLS, apiClass) ?? {};\n return Object.values(tools);\n}\n"]}
|
|
@@ -12,8 +12,14 @@ import { WpMcpToolHints } from './McpMetadata';
|
|
|
12
12
|
* which it is made of, are already here for the same reason.
|
|
13
13
|
*/
|
|
14
14
|
export declare class McpToolDefinition {
|
|
15
|
-
/** The stable protocol name from `@WpMcpTool('
|
|
15
|
+
/** The stable protocol name from `@WpMcpTool('<name>', '<title>')`. */
|
|
16
16
|
readonly name: string;
|
|
17
|
+
/**
|
|
18
|
+
* The human-readable display name from `@WpMcpTool('<name>', '<title>')`. REQUIRED (#1180):
|
|
19
|
+
* `tools/list` publishes it as both `Tool.title` and `annotations.title`, and a catalog entry
|
|
20
|
+
* without one is refused on load rather than silently publishing an untitled tool.
|
|
21
|
+
*/
|
|
22
|
+
readonly title: string;
|
|
17
23
|
/** The contract method it was rendered from, so a mismatch report can name the source. */
|
|
18
24
|
readonly methodName: string;
|
|
19
25
|
/**
|
|
@@ -31,8 +37,14 @@ export declare class McpToolDefinition {
|
|
|
31
37
|
readonly inputSchema: ApiJsonSchema;
|
|
32
38
|
readonly outputSchema: ApiJsonSchema;
|
|
33
39
|
constructor(
|
|
34
|
-
/** The stable protocol name from `@WpMcpTool('
|
|
40
|
+
/** The stable protocol name from `@WpMcpTool('<name>', '<title>')`. */
|
|
35
41
|
name: string,
|
|
42
|
+
/**
|
|
43
|
+
* The human-readable display name from `@WpMcpTool('<name>', '<title>')`. REQUIRED (#1180):
|
|
44
|
+
* `tools/list` publishes it as both `Tool.title` and `annotations.title`, and a catalog entry
|
|
45
|
+
* without one is refused on load rather than silently publishing an untitled tool.
|
|
46
|
+
*/
|
|
47
|
+
title: string,
|
|
36
48
|
/** The contract method it was rendered from, so a mismatch report can name the source. */
|
|
37
49
|
methodName: string,
|
|
38
50
|
/**
|
|
@@ -16,14 +16,21 @@ const McpMetadata_1 = require("./McpMetadata");
|
|
|
16
16
|
*/
|
|
17
17
|
class McpToolDefinition {
|
|
18
18
|
name;
|
|
19
|
+
title;
|
|
19
20
|
methodName;
|
|
20
21
|
description;
|
|
21
22
|
hints;
|
|
22
23
|
inputSchema;
|
|
23
24
|
outputSchema;
|
|
24
25
|
constructor(
|
|
25
|
-
/** The stable protocol name from `@WpMcpTool('
|
|
26
|
+
/** The stable protocol name from `@WpMcpTool('<name>', '<title>')`. */
|
|
26
27
|
name,
|
|
28
|
+
/**
|
|
29
|
+
* The human-readable display name from `@WpMcpTool('<name>', '<title>')`. REQUIRED (#1180):
|
|
30
|
+
* `tools/list` publishes it as both `Tool.title` and `annotations.title`, and a catalog entry
|
|
31
|
+
* without one is refused on load rather than silently publishing an untitled tool.
|
|
32
|
+
*/
|
|
33
|
+
title,
|
|
27
34
|
/** The contract method it was rendered from, so a mismatch report can name the source. */
|
|
28
35
|
methodName,
|
|
29
36
|
/**
|
|
@@ -39,6 +46,7 @@ class McpToolDefinition {
|
|
|
39
46
|
*/
|
|
40
47
|
hints, inputSchema, outputSchema) {
|
|
41
48
|
this.name = name;
|
|
49
|
+
this.title = title;
|
|
42
50
|
this.methodName = methodName;
|
|
43
51
|
this.description = description;
|
|
44
52
|
this.hints = hints;
|
|
@@ -100,7 +108,7 @@ class McpToolCatalogFile {
|
|
|
100
108
|
for (const tool of tools) {
|
|
101
109
|
if (byName.has(tool.name)) {
|
|
102
110
|
throw new McpToolCatalogError(`Duplicate MCP tool name '${tool.name}' in the generated catalog of ${contractName}.`, 'Tool names are the protocol identity and must be globally unique — rename one ' +
|
|
103
|
-
"of the two @WpMcpTool('
|
|
111
|
+
"of the two @WpMcpTool('<name>', '<title>') declarations.");
|
|
104
112
|
}
|
|
105
113
|
byName.set(tool.name, tool);
|
|
106
114
|
}
|
|
@@ -175,7 +183,7 @@ class McpCatalogJson {
|
|
|
175
183
|
toolFrom(entry) {
|
|
176
184
|
const record = this.record(entry, 'a tool definition');
|
|
177
185
|
const hints = this.record(record['hints'], 'a tool definition hints block');
|
|
178
|
-
return new McpToolDefinition(this.text(record['name'], 'name'), this.text(record['methodName'], 'methodName'), this.text(record['description'], 'description'), new McpMetadata_1.WpMcpToolHints(hints['readOnlyHint'] === true, hints['destructiveHint'] === true, hints['idempotentHint'] === true, hints['openWorldHint'] === true), this.schemaFrom(record['inputSchema']), this.schemaFrom(record['outputSchema']));
|
|
186
|
+
return new McpToolDefinition(this.text(record['name'], 'name'), this.title(record['title'], record['name']), this.text(record['methodName'], 'methodName'), this.text(record['description'], 'description'), new McpMetadata_1.WpMcpToolHints(hints['readOnlyHint'] === true, hints['destructiveHint'] === true, hints['idempotentHint'] === true, hints['openWorldHint'] === true), this.schemaFrom(record['inputSchema']), this.schemaFrom(record['outputSchema']));
|
|
179
187
|
}
|
|
180
188
|
// webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs
|
|
181
189
|
schemaFrom(value) {
|
|
@@ -240,6 +248,19 @@ class McpCatalogJson {
|
|
|
240
248
|
// webpieces-disable no-any-unknown -- the narrowed result of the check above
|
|
241
249
|
return value;
|
|
242
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* The REQUIRED, non-blank `title` (#1180). A catalog generated before titles existed has none, and
|
|
253
|
+
* is refused here so a stale artifact fails the boot loudly instead of publishing untitled tools.
|
|
254
|
+
*/
|
|
255
|
+
// webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs
|
|
256
|
+
title(value, name) {
|
|
257
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
258
|
+
throw new McpToolCatalogError(`The MCP tool catalog has no 'title' on tool '${String(name)}'; every tool needs a ` +
|
|
259
|
+
"human-readable title from @WpMcpTool('<name>', '<title>'). The catalog predates " +
|
|
260
|
+
'required titles (#1180).', REGENERATE);
|
|
261
|
+
}
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
243
264
|
// webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs
|
|
244
265
|
text(value, field) {
|
|
245
266
|
if (typeof value !== 'string') {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpToolCatalogFile.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/mcp/McpToolCatalogFile.ts"],"names":[],"mappings":";;;AAAA,2CAA2F;AAC3F,+CAA+C;AAE/C;;;;;;;;;;GAUG;AACH,MAAa,iBAAiB;IAGb;IAEA;IAMA;IAMA;IACA;IACA;IAlBb;IACI,yDAAyD;IAChD,IAAY;IACrB,0FAA0F;IACjF,UAAkB;IAC3B;;;;OAIG;IACM,WAAmB;IAC5B;;;;OAIG;IACM,KAAqB,EACrB,WAA0B,EAC1B,YAA2B;QAhB3B,SAAI,GAAJ,IAAI,CAAQ;QAEZ,eAAU,GAAV,UAAU,CAAQ;QAMlB,gBAAW,GAAX,WAAW,CAAQ;QAMnB,UAAK,GAAL,KAAK,CAAgB;QACrB,gBAAW,GAAX,WAAW,CAAe;QAC1B,iBAAY,GAAZ,YAAY,CAAe;IACrC,CAAC;CACP;AArBD,8CAqBC;AAED,uEAAuE;AACvE,MAAa,mBAAoB,SAAQ,KAAK;IAI7B;IAHb,YACI,OAAe;IACf,2CAA2C;IAClC,IAAY;QAErB,KAAK,CAAC,GAAG,OAAO,IAAI,IAAI,EAAE,CAAC,CAAC;QAFnB,SAAI,GAAJ,IAAI,CAAQ;QAGrB,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAClC,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACtD,CAAC;CACJ;AAVD,kDAUC;AAED,8FAA8F;AAC9F,MAAM,UAAU,GACZ,yFAAyF;IACzF,oFAAoF,CAAC;AAEzF,MAAM,WAAW,GAAG,MAAM,CAAC;AAC3B,MAAM,WAAW,GAAG,aAAa,CAAC;AAElC,kGAAkG;AAClG,MAAM,aAAa,GAAG,4BAA4B,CAAC;AAEnD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAa,kBAAkB;IAKd;IACA;IALI,MAAM,CAAyC;IAEhE;IACI,oGAAoG;IAC3F,YAAoB,EACpB,KAAmC;QADnC,iBAAY,GAAZ,YAAY,CAAQ;QACpB,UAAK,GAAL,KAAK,CAA8B;QAE5C,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;YACpC,MAAM,IAAI,mBAAmB,CACzB,IAAI,YAAY,wEAAwE,EACxF,UAAU,CACb,CAAC;QACN,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,GAAG,EAA6B,CAAC;QACpD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACxB,MAAM,IAAI,mBAAmB,CACzB,4BAA4B,IAAI,CAAC,IAAI,iCAAiC,YAAY,GAAG,EACrF,gFAAgF;oBAC5E,4CAA4C,CACnD,CAAC;YACN,CAAC;YACD,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAChC,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;IAED,wCAAwC;IACxC,IAAI,QAAQ;QACR,OAAO,kBAAkB,CAAC,WAAW,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC7D,CAAC;IAED,IAAI,CAAC,IAAY;QACb,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAED,yFAAyF;IACzF,KAAK;QACD,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1C,CAAC;IAED,iFAAiF;IACjF,UAAU;QACN,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,IAAuB,EAAE,KAAwB,EAAE,EAAE,CACrF,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CACtC,CAAC;QACF,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC;IACtD,CAAC;IAED,+EAA+E;IAC/E,6EAA6E;IAC7E,MAAM,CAAC,WAAW,CAAC,YAAoB;QACnC,OAAO,GAAG,WAAW,GAAG,YAAY,GAAG,WAAW,EAAE,CAAC;IACzD,CAAC;IAED,oGAAoG;IACpG,6EAA6E;IAC7E,MAAM,CAAC,cAAc,CAAC,QAAgB;QAClC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC;YAAE,OAAO,SAAS,CAAC;QAC3F,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;QACtF,OAAO,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IACvD,CAAC;IAED;;;;;;;OAOG;IACH,8EAA8E;IAC9E,MAAM,CAAC,YAAY,CAAC,QAAgB,EAAE,IAAY;QAC9C,MAAM,YAAY,GAAG,kBAAkB,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;QACjE,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,mBAAmB,CACzB,IAAI,QAAQ,yDAAyD;gBACjE,GAAG,WAAW,kBAAkB,WAAW,GAAG,EAClD,UAAU,CACb,CAAC;QACN,CAAC;QACD,OAAO,IAAI,kBAAkB,CAAC,YAAY,EAAE,IAAI,cAAc,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAClF,CAAC;CACJ;AApFD,gDAoFC;AAED;;;;;;GAMG;AACH,MAAM,cAAc;IAChB,KAAK,CAAC,IAAY;QACd,sGAAsG;QACtG,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,mBAAmB,CACzB,gEAAgE,EAChE,UAAU,CACb,CAAC;QACN,CAAC;QACD,oFAAoF;QACpF,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IAChE,CAAC;IAED,oFAAoF;IAC5E,QAAQ,CAAC,KAAc;QAC3B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC;QACvD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,+BAA+B,CAAC,CAAC;QAC5E,OAAO,IAAI,iBAAiB,CACxB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EACjC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,YAAY,CAAC,EAC7C,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC,EAC/C,IAAI,4BAAc,CACd,KAAK,CAAC,cAAc,CAAC,KAAK,IAAI,EAC9B,KAAK,CAAC,iBAAiB,CAAC,KAAK,IAAI,EACjC,KAAK,CAAC,gBAAgB,CAAC,KAAK,IAAI,EAChC,KAAK,CAAC,eAAe,CAAC,KAAK,IAAI,CAClC,EACD,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,EACtC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAC1C,CAAC;IACN,CAAC;IAED,oFAAoF;IAC5E,UAAU,CAAC,KAAc;QAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;QACnD,MAAM,MAAM,GAAG,IAAI,yBAAa,CAC5B,MAAM,CAAC,MAAM,CAAqD,CACrE,CAAC;QACF,IAAI,OAAO,MAAM,CAAC,aAAa,CAAC,KAAK,QAAQ;YAAE,MAAM,CAAC,WAAW,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC;QAC1F,IAAI,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,QAAQ;YAAE,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;QAC9E,IAAI,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,QAAQ;YAAE,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;QAC9E,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YAAE,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAa,CAAC;QACxF,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAAE,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,CAAa,CAAC;QAC5E,IAAI,OAAO,MAAM,CAAC,cAAc,CAAC,KAAK,QAAQ,EAAE,CAAC;YAC7C,MAAM,CAAC,cAAc,CAAC,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC;QACpD,CAAC;QACD,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,SAAS;YAAE,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;QACnF,MAAM,KAAK,GAAG,MAAM,CAAC,sBAAsB,CAAC,CAAC;QAC7C,IAAI,OAAO,KAAK,KAAK,SAAS;YAAE,MAAM,CAAC,oBAAoB,GAAG,KAAK,CAAC;aAC/D,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,CAAC,oBAAoB,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACnF,MAAM,UAAU,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;QACxC,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAkC,EAAE,CAAC;YAChD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CACrC,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,2BAA2B,CAAC,CACvD,EAAE,CAAC;gBACA,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;YACxC,CAAC;YACD,MAAM,CAAC,UAAU,GAAG,KAAK,CAAC;QAC9B,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;QAC9B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACvB,oFAAoF;YACpF,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,MAAe,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;YACvE,MAAM,CAAC,aAAa,GAAG,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,MAAM,CAAC;IAClB,CAAC;IAED;;;OAGG;IACH,oFAAoF;IAC5E,iBAAiB,CAAC,KAAc;QACpC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,8BAA8B,CAAC,CAAC;QAClE,MAAM,OAAO,GAA2B,EAAE,CAAC;QAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CACtC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,+BAA+B,CAAC,CAClE,EAAE,CAAC;YACA,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,0BAA0B,GAAG,GAAG,CAAC,CAAC;QACvE,CAAC;QACD,OAAO,IAAI,sCAA0B,CACjC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,EAAE,4BAA4B,CAAC,EAC/D,OAAO,CACV,CAAC;IACN,CAAC;IAED,oFAAoF;IAC5E,MAAM,CAAC,KAAc,EAAE,IAAY;QACvC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACtE,MAAM,IAAI,mBAAmB,CAAC,mCAAmC,IAAI,GAAG,EAAE,UAAU,CAAC,CAAC;QAC1F,CAAC;QACD,6EAA6E;QAC7E,OAAO,KAAgC,CAAC;IAC5C,CAAC;IAED,oFAAoF;IAC5E,IAAI,CAAC,KAAc,EAAE,KAAa;QACtC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC5B,MAAM,IAAI,mBAAmB,CACzB,gCAAgC,KAAK,yBAAyB,EAC9D,UAAU,CACb,CAAC;QACN,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;CACJ","sourcesContent":["import { ApiJsonSchema, ApiJsonSchemaDiscriminator, ApiJsonSchemaType } from './DtoSchema';\nimport { WpMcpToolHints } from './McpMetadata';\n\n/**\n * ONE MCP tool, as `tools/list` publishes it.\n *\n * It is BUILT from the source by `@webpieces/api-doc-model`'s `McpSchemaRenderer` at build time, and\n * READ here at boot — so the tool an agent is shown and the tool the server accepts are the same\n * bytes by construction, not by two declarations agreeing.\n *\n * It lives in `core-util` rather than in the extractor package because both ends need it and only\n * one end may depend on the TypeScript compiler API. `ApiJsonSchema` and {@link WpMcpToolHints},\n * which it is made of, are already here for the same reason.\n */\nexport class McpToolDefinition {\n constructor(\n /** The stable protocol name from `@WpMcpTool('...')`. */\n readonly name: string,\n /** The contract method it was rendered from, so a mismatch report can name the source. */\n readonly methodName: string,\n /**\n * The tool documentation: the method's `@mcp` tag when it has one, its JSDoc body otherwise.\n * Documentation has exactly ONE source — the doc comment above the declaration — for the\n * partner reading OpenAPI and the agent reading `tools/list` alike.\n */\n readonly description: string,\n /**\n * All four hints. Three are COMPUTED from `@Endpoint`'s `operation` by\n * `mcpHintsForOperation`, which is the one place that mapping lives; `openWorldHint` comes\n * from `@Endpoint`'s `openWorld` option.\n */\n readonly hints: WpMcpToolHints,\n readonly inputSchema: ApiJsonSchema,\n readonly outputSchema: ApiJsonSchema,\n ) {}\n}\n\n/** Thrown when a catalog is not one this release can read or write. */\nexport class McpToolCatalogError extends Error {\n constructor(\n message: string,\n /** What to do instead, in one sentence. */\n readonly cure: string,\n ) {\n super(`${message} ${cure}`);\n this.name = 'McpToolCatalogError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** The one cure every catalog failure prescribes: rebuild the artifact from the contracts. */\nconst REGENERATE =\n 'Rebuild the api library that declares the contract (its `openapi-generate` target runs ' +\n '`wp-openapi`, which writes one `mcp-<ContractClass>-tools.json` per MCP contract).';\n\nconst FILE_PREFIX = 'mcp-';\nconst FILE_SUFFIX = '-tools.json';\n\n/** A class name, which is what the file name carries. Anything else is not one of these files. */\nconst CONTRACT_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * ONE contract's MCP tools, keyed by protocol name — the artifact `wp-openapi` writes as\n * `mcp-<ContractClass>-tools.json`, one file per contract that declares `MCP` and has at least one\n * `@WpMcpTool`, beside the library's OpenAPI documents.\n *\n * ## Why one file per CONTRACT and not one per library\n *\n * One api library holds many contracts (it is how a public partner API is grouped), and a server binds\n * contracts, not libraries — `McpApiBinding.local(LangCourseAuthorApi, ...)`. With one file per\n * contract the server can check every binding against exactly the file generated from THAT contract,\n * and refuse a file it was handed for a contract it does not bind. A single per-library file could only\n * say \"some tool is missing\" (#1021).\n *\n * This is the browser-safe CODEC — the bytes and the file name. Reading the files from an installed\n * package is `McpToolCatalog.fromPackages` in `@webpieces/mcp-server`, because only a node process\n * has a filesystem to read them from.\n */\nexport class McpToolCatalogFile {\n private readonly byName: ReadonlyMap<string, McpToolDefinition>;\n\n constructor(\n /** The contract CLASS name — `binding.api.name` at runtime, the class declaration at build time. */\n readonly contractName: string,\n readonly tools: readonly McpToolDefinition[],\n ) {\n if (!CONTRACT_NAME.test(contractName)) {\n throw new McpToolCatalogError(\n `'${contractName}' is not a contract class name, so it cannot name an MCP tool catalog.`,\n REGENERATE,\n );\n }\n const byName = new Map<string, McpToolDefinition>();\n for (const tool of tools) {\n if (byName.has(tool.name)) {\n throw new McpToolCatalogError(\n `Duplicate MCP tool name '${tool.name}' in the generated catalog of ${contractName}.`,\n 'Tool names are the protocol identity and must be globally unique — rename one ' +\n \"of the two @WpMcpTool('...') declarations.\",\n );\n }\n byName.set(tool.name, tool);\n }\n this.byName = byName;\n }\n\n /** `mcp-<ContractClass>-tools.json`. */\n get fileName(): string {\n return McpToolCatalogFile.fileNameFor(this.contractName);\n }\n\n find(name: string): McpToolDefinition | undefined {\n return this.byName.get(name);\n }\n\n /** The names this catalog carries, sorted, so a failure can print what WAS published. */\n names(): readonly string[] {\n return [...this.byName.keys()].sort();\n }\n\n /** The bytes `wp-openapi` writes. Sorted by name so the artifact is diffable. */\n toJsonText(): string {\n const tools = [...this.tools].sort((left: McpToolDefinition, right: McpToolDefinition) =>\n left.name.localeCompare(right.name),\n );\n return `${JSON.stringify(tools, undefined, 2)}\\n`;\n }\n\n /** The file name the catalog of `contractName` is written to and read from. */\n // webpieces-disable no-function-outside-class -- static helper of this class\n static fileNameFor(contractName: string): string {\n return `${FILE_PREFIX}${contractName}${FILE_SUFFIX}`;\n }\n\n /** The contract a file name is the catalog of, or `undefined` when it is not one of these files. */\n // webpieces-disable no-function-outside-class -- static helper of this class\n static contractNameOf(fileName: string): string | undefined {\n if (!fileName.startsWith(FILE_PREFIX) || !fileName.endsWith(FILE_SUFFIX)) return undefined;\n const name = fileName.slice(FILE_PREFIX.length, fileName.length - FILE_SUFFIX.length);\n return CONTRACT_NAME.test(name) ? name : undefined;\n }\n\n /**\n * Reads one `mcp-<ContractClass>-tools.json`. The contract is the one the FILE NAME names. Every\n * nested value is rebuilt as a real class instance, because this is the one place untyped bytes\n * become the schemas a live protocol surface publishes.\n *\n * Malformed JSON is not caught here: it throws `SyntaxError` to the caller's own top-level\n * handler, which is where every other read-the-world failure in webpieces is reported.\n */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromJsonText(fileName: string, text: string): McpToolCatalogFile {\n const contractName = McpToolCatalogFile.contractNameOf(fileName);\n if (contractName === undefined) {\n throw new McpToolCatalogError(\n `'${fileName}' is not an MCP tool catalog file name; they are named ` +\n `${FILE_PREFIX}<ContractClass>${FILE_SUFFIX}.`,\n REGENERATE,\n );\n }\n return new McpToolCatalogFile(contractName, new McpCatalogJson().parse(text));\n }\n}\n\n/**\n * The PARSE BOUNDARY: untyped bytes in, real class instances out.\n *\n * A class of its own rather than statics on {@link McpToolCatalogFile} so every step is an ordinary\n * instance method — and because it is the one place in this file where `unknown` is the correct type\n * rather than a missing one. Nothing else in webpieces should ever hold a half-narrowed schema.\n */\nclass McpCatalogJson {\n parse(text: string): readonly McpToolDefinition[] {\n // webpieces-disable no-any-unknown -- this IS the untyped-bytes boundary; every step below narrows it\n const parsed: unknown = JSON.parse(text);\n if (!Array.isArray(parsed)) {\n throw new McpToolCatalogError(\n 'The MCP tool catalog must be a JSON array of tool definitions.',\n REGENERATE,\n );\n }\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n return parsed.map((entry: unknown) => this.toolFrom(entry));\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private toolFrom(entry: unknown): McpToolDefinition {\n const record = this.record(entry, 'a tool definition');\n const hints = this.record(record['hints'], 'a tool definition hints block');\n return new McpToolDefinition(\n this.text(record['name'], 'name'),\n this.text(record['methodName'], 'methodName'),\n this.text(record['description'], 'description'),\n new WpMcpToolHints(\n hints['readOnlyHint'] === true,\n hints['destructiveHint'] === true,\n hints['idempotentHint'] === true,\n hints['openWorldHint'] === true,\n ),\n this.schemaFrom(record['inputSchema']),\n this.schemaFrom(record['outputSchema']),\n );\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private schemaFrom(value: unknown): ApiJsonSchema {\n const record = this.record(value, 'a JSON schema');\n const schema = new ApiJsonSchema(\n record['type'] as ApiJsonSchemaType | readonly ApiJsonSchemaType[],\n );\n if (typeof record['description'] === 'string') schema.description = record['description'];\n if (typeof record['minimum'] === 'number') schema.minimum = record['minimum'];\n if (typeof record['maximum'] === 'number') schema.maximum = record['maximum'];\n if (Array.isArray(record['required'])) schema.required = record['required'] as string[];\n if (Array.isArray(record['enum'])) schema.enum = record['enum'] as string[];\n if (typeof record['x-mcp-header'] === 'string') {\n schema['x-mcp-header'] = record['x-mcp-header'];\n }\n if (record['items'] !== undefined) schema.items = this.schemaFrom(record['items']);\n const extra = record['additionalProperties'];\n if (typeof extra === 'boolean') schema.additionalProperties = extra;\n else if (extra !== undefined) schema.additionalProperties = this.schemaFrom(extra);\n const properties = record['properties'];\n if (properties !== undefined) {\n const built: Record<string, ApiJsonSchema> = {};\n for (const [key, child] of Object.entries(\n this.record(properties, 'a schema properties block'),\n )) {\n built[key] = this.schemaFrom(child);\n }\n schema.properties = built;\n }\n const oneOf = record['oneOf'];\n if (Array.isArray(oneOf)) {\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n schema.oneOf = oneOf.map((branch: unknown) => this.schemaFrom(branch));\n schema.discriminator = this.discriminatorFrom(record['discriminator']);\n }\n return schema;\n }\n\n /**\n * The `discriminator` block, or undefined — a union TypeScript cannot narrow is published WITHOUT\n * one, so an absent block is a legal document and not a missing field.\n */\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private discriminatorFrom(value: unknown): ApiJsonSchemaDiscriminator | undefined {\n if (value === undefined) return undefined;\n const record = this.record(value, 'a schema discriminator block');\n const mapping: Record<string, string> = {};\n for (const [key, branch] of Object.entries(\n this.record(record['mapping'], 'a discriminator mapping block'),\n )) {\n mapping[key] = this.text(branch, `discriminator mapping '${key}'`);\n }\n return new ApiJsonSchemaDiscriminator(\n this.text(record['propertyName'], 'discriminator propertyName'),\n mapping,\n );\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private record(value: unknown, what: string): Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new McpToolCatalogError(`The MCP tool catalog is missing ${what}.`, REGENERATE);\n }\n // webpieces-disable no-any-unknown -- the narrowed result of the check above\n return value as Record<string, unknown>;\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private text(value: unknown, field: string): string {\n if (typeof value !== 'string') {\n throw new McpToolCatalogError(\n `The MCP tool catalog has no '${field}' on a tool definition.`,\n REGENERATE,\n );\n }\n return value;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"McpToolCatalogFile.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/mcp/McpToolCatalogFile.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiJsonSchema, ApiJsonSchemaDiscriminator, ApiJsonSchemaType } from './DtoSchema';\nimport { WpMcpToolHints } from './McpMetadata';\n\n/**\n * ONE MCP tool, as `tools/list` publishes it.\n *\n * It is BUILT from the source by `@webpieces/api-doc-model`'s `McpSchemaRenderer` at build time, and\n * READ here at boot — so the tool an agent is shown and the tool the server accepts are the same\n * bytes by construction, not by two declarations agreeing.\n *\n * It lives in `core-util` rather than in the extractor package because both ends need it and only\n * one end may depend on the TypeScript compiler API. `ApiJsonSchema` and {@link WpMcpToolHints},\n * which it is made of, are already here for the same reason.\n */\nexport class McpToolDefinition {\n constructor(\n /** The stable protocol name from `@WpMcpTool('<name>', '<title>')`. */\n readonly name: string,\n /**\n * The human-readable display name from `@WpMcpTool('<name>', '<title>')`. REQUIRED (#1180):\n * `tools/list` publishes it as both `Tool.title` and `annotations.title`, and a catalog entry\n * without one is refused on load rather than silently publishing an untitled tool.\n */\n readonly title: string,\n /** The contract method it was rendered from, so a mismatch report can name the source. */\n readonly methodName: string,\n /**\n * The tool documentation: the method's `@mcp` tag when it has one, its JSDoc body otherwise.\n * Documentation has exactly ONE source — the doc comment above the declaration — for the\n * partner reading OpenAPI and the agent reading `tools/list` alike.\n */\n readonly description: string,\n /**\n * All four hints. Three are COMPUTED from `@Endpoint`'s `operation` by\n * `mcpHintsForOperation`, which is the one place that mapping lives; `openWorldHint` comes\n * from `@Endpoint`'s `openWorld` option.\n */\n readonly hints: WpMcpToolHints,\n readonly inputSchema: ApiJsonSchema,\n readonly outputSchema: ApiJsonSchema,\n ) {}\n}\n\n/** Thrown when a catalog is not one this release can read or write. */\nexport class McpToolCatalogError extends Error {\n constructor(\n message: string,\n /** What to do instead, in one sentence. */\n readonly cure: string,\n ) {\n super(`${message} ${cure}`);\n this.name = 'McpToolCatalogError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** The one cure every catalog failure prescribes: rebuild the artifact from the contracts. */\nconst REGENERATE =\n 'Rebuild the api library that declares the contract (its `openapi-generate` target runs ' +\n '`wp-openapi`, which writes one `mcp-<ContractClass>-tools.json` per MCP contract).';\n\nconst FILE_PREFIX = 'mcp-';\nconst FILE_SUFFIX = '-tools.json';\n\n/** A class name, which is what the file name carries. Anything else is not one of these files. */\nconst CONTRACT_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * ONE contract's MCP tools, keyed by protocol name — the artifact `wp-openapi` writes as\n * `mcp-<ContractClass>-tools.json`, one file per contract that declares `MCP` and has at least one\n * `@WpMcpTool`, beside the library's OpenAPI documents.\n *\n * ## Why one file per CONTRACT and not one per library\n *\n * One api library holds many contracts (it is how a public partner API is grouped), and a server binds\n * contracts, not libraries — `McpApiBinding.local(LangCourseAuthorApi, ...)`. With one file per\n * contract the server can check every binding against exactly the file generated from THAT contract,\n * and refuse a file it was handed for a contract it does not bind. A single per-library file could only\n * say \"some tool is missing\" (#1021).\n *\n * This is the browser-safe CODEC — the bytes and the file name. Reading the files from an installed\n * package is `McpToolCatalog.fromPackages` in `@webpieces/mcp-server`, because only a node process\n * has a filesystem to read them from.\n */\nexport class McpToolCatalogFile {\n private readonly byName: ReadonlyMap<string, McpToolDefinition>;\n\n constructor(\n /** The contract CLASS name — `binding.api.name` at runtime, the class declaration at build time. */\n readonly contractName: string,\n readonly tools: readonly McpToolDefinition[],\n ) {\n if (!CONTRACT_NAME.test(contractName)) {\n throw new McpToolCatalogError(\n `'${contractName}' is not a contract class name, so it cannot name an MCP tool catalog.`,\n REGENERATE,\n );\n }\n const byName = new Map<string, McpToolDefinition>();\n for (const tool of tools) {\n if (byName.has(tool.name)) {\n throw new McpToolCatalogError(\n `Duplicate MCP tool name '${tool.name}' in the generated catalog of ${contractName}.`,\n 'Tool names are the protocol identity and must be globally unique — rename one ' +\n \"of the two @WpMcpTool('<name>', '<title>') declarations.\",\n );\n }\n byName.set(tool.name, tool);\n }\n this.byName = byName;\n }\n\n /** `mcp-<ContractClass>-tools.json`. */\n get fileName(): string {\n return McpToolCatalogFile.fileNameFor(this.contractName);\n }\n\n find(name: string): McpToolDefinition | undefined {\n return this.byName.get(name);\n }\n\n /** The names this catalog carries, sorted, so a failure can print what WAS published. */\n names(): readonly string[] {\n return [...this.byName.keys()].sort();\n }\n\n /** The bytes `wp-openapi` writes. Sorted by name so the artifact is diffable. */\n toJsonText(): string {\n const tools = [...this.tools].sort((left: McpToolDefinition, right: McpToolDefinition) =>\n left.name.localeCompare(right.name),\n );\n return `${JSON.stringify(tools, undefined, 2)}\\n`;\n }\n\n /** The file name the catalog of `contractName` is written to and read from. */\n // webpieces-disable no-function-outside-class -- static helper of this class\n static fileNameFor(contractName: string): string {\n return `${FILE_PREFIX}${contractName}${FILE_SUFFIX}`;\n }\n\n /** The contract a file name is the catalog of, or `undefined` when it is not one of these files. */\n // webpieces-disable no-function-outside-class -- static helper of this class\n static contractNameOf(fileName: string): string | undefined {\n if (!fileName.startsWith(FILE_PREFIX) || !fileName.endsWith(FILE_SUFFIX)) return undefined;\n const name = fileName.slice(FILE_PREFIX.length, fileName.length - FILE_SUFFIX.length);\n return CONTRACT_NAME.test(name) ? name : undefined;\n }\n\n /**\n * Reads one `mcp-<ContractClass>-tools.json`. The contract is the one the FILE NAME names. Every\n * nested value is rebuilt as a real class instance, because this is the one place untyped bytes\n * become the schemas a live protocol surface publishes.\n *\n * Malformed JSON is not caught here: it throws `SyntaxError` to the caller's own top-level\n * handler, which is where every other read-the-world failure in webpieces is reported.\n */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromJsonText(fileName: string, text: string): McpToolCatalogFile {\n const contractName = McpToolCatalogFile.contractNameOf(fileName);\n if (contractName === undefined) {\n throw new McpToolCatalogError(\n `'${fileName}' is not an MCP tool catalog file name; they are named ` +\n `${FILE_PREFIX}<ContractClass>${FILE_SUFFIX}.`,\n REGENERATE,\n );\n }\n return new McpToolCatalogFile(contractName, new McpCatalogJson().parse(text));\n }\n}\n\n/**\n * The PARSE BOUNDARY: untyped bytes in, real class instances out.\n *\n * A class of its own rather than statics on {@link McpToolCatalogFile} so every step is an ordinary\n * instance method — and because it is the one place in this file where `unknown` is the correct type\n * rather than a missing one. Nothing else in webpieces should ever hold a half-narrowed schema.\n */\nclass McpCatalogJson {\n parse(text: string): readonly McpToolDefinition[] {\n // webpieces-disable no-any-unknown -- this IS the untyped-bytes boundary; every step below narrows it\n const parsed: unknown = JSON.parse(text);\n if (!Array.isArray(parsed)) {\n throw new McpToolCatalogError(\n 'The MCP tool catalog must be a JSON array of tool definitions.',\n REGENERATE,\n );\n }\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n return parsed.map((entry: unknown) => this.toolFrom(entry));\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private toolFrom(entry: unknown): McpToolDefinition {\n const record = this.record(entry, 'a tool definition');\n const hints = this.record(record['hints'], 'a tool definition hints block');\n return new McpToolDefinition(\n this.text(record['name'], 'name'),\n this.title(record['title'], record['name']),\n this.text(record['methodName'], 'methodName'),\n this.text(record['description'], 'description'),\n new WpMcpToolHints(\n hints['readOnlyHint'] === true,\n hints['destructiveHint'] === true,\n hints['idempotentHint'] === true,\n hints['openWorldHint'] === true,\n ),\n this.schemaFrom(record['inputSchema']),\n this.schemaFrom(record['outputSchema']),\n );\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private schemaFrom(value: unknown): ApiJsonSchema {\n const record = this.record(value, 'a JSON schema');\n const schema = new ApiJsonSchema(\n record['type'] as ApiJsonSchemaType | readonly ApiJsonSchemaType[],\n );\n if (typeof record['description'] === 'string') schema.description = record['description'];\n if (typeof record['minimum'] === 'number') schema.minimum = record['minimum'];\n if (typeof record['maximum'] === 'number') schema.maximum = record['maximum'];\n if (Array.isArray(record['required'])) schema.required = record['required'] as string[];\n if (Array.isArray(record['enum'])) schema.enum = record['enum'] as string[];\n if (typeof record['x-mcp-header'] === 'string') {\n schema['x-mcp-header'] = record['x-mcp-header'];\n }\n if (record['items'] !== undefined) schema.items = this.schemaFrom(record['items']);\n const extra = record['additionalProperties'];\n if (typeof extra === 'boolean') schema.additionalProperties = extra;\n else if (extra !== undefined) schema.additionalProperties = this.schemaFrom(extra);\n const properties = record['properties'];\n if (properties !== undefined) {\n const built: Record<string, ApiJsonSchema> = {};\n for (const [key, child] of Object.entries(\n this.record(properties, 'a schema properties block'),\n )) {\n built[key] = this.schemaFrom(child);\n }\n schema.properties = built;\n }\n const oneOf = record['oneOf'];\n if (Array.isArray(oneOf)) {\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n schema.oneOf = oneOf.map((branch: unknown) => this.schemaFrom(branch));\n schema.discriminator = this.discriminatorFrom(record['discriminator']);\n }\n return schema;\n }\n\n /**\n * The `discriminator` block, or undefined — a union TypeScript cannot narrow is published WITHOUT\n * one, so an absent block is a legal document and not a missing field.\n */\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private discriminatorFrom(value: unknown): ApiJsonSchemaDiscriminator | undefined {\n if (value === undefined) return undefined;\n const record = this.record(value, 'a schema discriminator block');\n const mapping: Record<string, string> = {};\n for (const [key, branch] of Object.entries(\n this.record(record['mapping'], 'a discriminator mapping block'),\n )) {\n mapping[key] = this.text(branch, `discriminator mapping '${key}'`);\n }\n return new ApiJsonSchemaDiscriminator(\n this.text(record['propertyName'], 'discriminator propertyName'),\n mapping,\n );\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private record(value: unknown, what: string): Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new McpToolCatalogError(`The MCP tool catalog is missing ${what}.`, REGENERATE);\n }\n // webpieces-disable no-any-unknown -- the narrowed result of the check above\n return value as Record<string, unknown>;\n }\n\n /**\n * The REQUIRED, non-blank `title` (#1180). A catalog generated before titles existed has none, and\n * is refused here so a stale artifact fails the boot loudly instead of publishing untitled tools.\n */\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private title(value: unknown, name: unknown): string {\n if (typeof value !== 'string' || value.trim() === '') {\n throw new McpToolCatalogError(\n `The MCP tool catalog has no 'title' on tool '${String(name)}'; every tool needs a ` +\n \"human-readable title from @WpMcpTool('<name>', '<title>'). The catalog predates \" +\n 'required titles (#1180).',\n REGENERATE,\n );\n }\n return value;\n }\n\n // webpieces-disable no-any-unknown -- parsing JSON is exactly where unknown belongs\n private text(value: unknown, field: string): string {\n if (typeof value !== 'string') {\n throw new McpToolCatalogError(\n `The MCP tool catalog has no '${field}' on a tool definition.`,\n REGENERATE,\n );\n }\n return value;\n }\n}\n"]}
|