@sezzlee/sdk-nestjs 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ An MCP layer that embeds into your existing NestJS backend. It exposes your endp
4
4
  a search-first tool catalog and replays each call through **your own** pipeline, so your guards,
5
5
  pipes and interceptors keep running unchanged. Not a gateway, not duplicated business logic.
6
6
 
7
- > Status: `0.2.0`, alpha; breaking changes are possible. [samples/demo-api](samples/demo-api) is
7
+ > Status: `0.2.1`, alpha; breaking changes are possible. [samples/demo-api](samples/demo-api) is
8
8
  > the reference wiring.
9
9
 
10
10
  ## Requirements
@@ -330,6 +330,13 @@ of BM25, bind a `ToolRanker` through `forRoot`'s overrides (`{ toolRanker: { use
330
330
  (`"fallback"` or `"error"`) govern it. Visibility, the tag filter and the payload budget still apply
331
331
  to its answer — see the how-to on plugging in your own search.
332
332
 
333
+ When an operation's behaviour changes behind an unchanged schema, raise its revision
334
+ (`@McpTool({ revision: 2 })` on the method, on the controller for every method in it, or
335
+ `options.revision` for the whole catalog). The revision enters the tool version, so during a rolling
336
+ deploy a call pinned to the old version is refused with `tool_changed` instead of running against
337
+ the new behaviour. The levels add up rather than replacing one another — see the how-to on shipping
338
+ a behaviour change mid-deploy.
339
+
333
340
  This repository ships a client:
334
341
 
335
342
  ```bash
package/dist/catalog.js CHANGED
@@ -12,7 +12,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
12
  };
13
13
  import { Inject, Injectable, Optional, RequestMethod, } from "@nestjs/common";
14
14
  import { ApplicationConfig, DiscoveryService, HttpAdapterHost, ModulesContainer, } from "@nestjs/core";
15
- import { assertCatalogValid, buildCatalog, combineMarkers, matchesRoute, SezzleeTemplateError, textMediaType, urlEncodedMediaType, } from "@sezzlee/core";
15
+ import { assertCatalogValid, buildCatalog, combineMarkers, declareRevision, matchesRoute, SezzleeTemplateError, textMediaType, urlEncodedMediaType, } from "@sezzlee/core";
16
16
  import { severityOf } from "./discovery/diagnostics.js";
17
17
  import { createRoutePaths, discoverEndpoints, modulePathOf, normalizeRoute, } from "./discovery/endpoint-discovery.js";
18
18
  import { loadFamilies, resolveFamily, } from "./families.js";
@@ -227,20 +227,41 @@ let SezzleeCatalog = class SezzleeCatalog {
227
227
  }
228
228
  return true;
229
229
  });
230
+ const catalogRevision = declareRevision("The catalog", { catalog: [{ value: this.options.revision, by: "options.revision" }] }, report)?.catalog;
230
231
  const built = buildCatalog(routed.map((endpoint) => {
231
- const tags = endpoint.hints.tags ?? this.options.tags?.(endpoint.controller.name);
232
- const searchTerms = endpoint.hints.searchTerms ??
233
- this.options.searchTerms?.(endpoint.controller.name);
232
+ const container = endpoint.controller.name;
233
+ const owner = `${container}.${endpoint.handlerName}`;
234
+ const tags = endpoint.hints.tags ?? this.options.tags?.(container);
235
+ const searchTerms = endpoint.hints.searchTerms ?? this.options.searchTerms?.(container);
234
236
  return {
235
237
  source: {
236
238
  controller: endpoint.controller,
237
239
  handlerName: endpoint.handlerName,
238
240
  },
239
- owner: `${endpoint.controller.name}.${endpoint.handlerName}`,
241
+ owner,
240
242
  descriptor: endpoint.descriptor,
241
243
  ...markersOf(endpoint),
242
244
  ...(tags === undefined ? {} : { tags }),
243
245
  ...(searchTerms === undefined ? {} : { searchTerms }),
246
+ revision: {
247
+ catalog: [{ value: catalogRevision, by: "options.revision" }],
248
+ container: [
249
+ {
250
+ value: endpoint.revisions.container,
251
+ by: `@McpTool({ revision }) on ${container}`,
252
+ },
253
+ {
254
+ value: this.options.containerRevision?.(container),
255
+ by: "options.containerRevision",
256
+ },
257
+ ],
258
+ operation: [
259
+ {
260
+ value: endpoint.revisions.operation,
261
+ by: `@McpTool({ revision }) on ${owner}`,
262
+ },
263
+ ],
264
+ },
244
265
  declare: (cleaned) => declaredDescriptor(endpoint, this.curationFor(endpoint), cleaned, families.get(endpoint)),
245
266
  };
246
267
  }), {
@@ -98,6 +98,13 @@ export interface McpToolOptions {
98
98
  * never a filter key and never shown to the agent ([search-semantics.md]).
99
99
  */
100
100
  readonly searchTerms?: readonly string[];
101
+ /**
102
+ * The behaviour revision at the level the decorator sits on: a method's covers that operation,
103
+ * a controller's every operation in it. Raised when behaviour changes behind an unchanged
104
+ * schema, it changes the tool version and nothing the agent reads ([metadata-contract.md]).
105
+ * The two levels never replace one another.
106
+ */
107
+ readonly revision?: number;
101
108
  /**
102
109
  * The body's media type, replacing the one discovery chooses. It is the host's own claim that a
103
110
  * parser for it is registered; when `@ApiConsumes` also lists types, it must be one of them.
@@ -45,6 +45,10 @@ export interface DiscoveredEndpoint {
45
45
  readonly containerMarkers: readonly boolean[];
46
46
  readonly operationMarkers: readonly boolean[];
47
47
  readonly hints: McpToolOptions;
48
+ readonly revisions: {
49
+ readonly container: number | undefined;
50
+ readonly operation: number | undefined;
51
+ };
48
52
  }
49
53
  export declare function discoverEndpoints(controllers: readonly ControllerCandidate[], options?: DiscoveryOptions): DiscoveredEndpoint[];
50
54
  /** `RouterModule` writes the module path under a key scoped to the application instance, falling back to the bare key. */
@@ -95,6 +95,10 @@ function fromController(candidate, options) {
95
95
  containerMarkers: containerMarkers.map((marker) => marker.include),
96
96
  operationMarkers: operationMarkers.map((marker) => marker.include),
97
97
  hints,
98
+ revisions: {
99
+ container: declaredRevision(containerMarkers),
100
+ operation: declaredRevision(operationMarkers),
101
+ },
98
102
  });
99
103
  }
100
104
  }
@@ -639,12 +643,21 @@ function argumentsOf(controller, handlerName) {
639
643
  return (Reflect.getMetadata(ROUTE_ARGS_METADATA, controller, handlerName) ??
640
644
  {});
641
645
  }
646
+ function declaredRevision(markers) {
647
+ let revision;
648
+ for (const marker of markers) {
649
+ revision = marker.options.revision ?? revision;
650
+ }
651
+ return revision;
652
+ }
642
653
  /**
643
- * Flat last-wins merge, except for curation.
654
+ * Flat last-wins merge, except for curation and the revision.
644
655
  *
645
656
  * `arguments` merges per argument and per field so a container-level rule that hides a tenant
646
657
  * identifier survives a method-level rule that only renames something else. A flat replacement
647
- * would silently unhide it.
658
+ * would silently unhide it. `revision` is kept out entirely: its levels are read apart
659
+ * ({@link declaredRevision}), because a method's revision replacing its controller's would hide a
660
+ * raised controller revision from that method's tool version.
648
661
  */
649
662
  function mergeHints(container, operation) {
650
663
  const merged = {};
@@ -652,7 +665,7 @@ function mergeHints(container, operation) {
652
665
  const variants = [];
653
666
  for (const marker of [...container, ...operation]) {
654
667
  for (const [key, value] of Object.entries(marker.options)) {
655
- if (value === undefined) {
668
+ if (value === undefined || key === "revision") {
656
669
  continue;
657
670
  }
658
671
  if (key === "arguments") {
package/dist/options.d.ts CHANGED
@@ -195,6 +195,10 @@ export declare class SezzleeOptions {
195
195
  tags?: (container: string) => readonly string[] | undefined;
196
196
  /** Search vocabulary for a container the host cannot decorate, below a `@McpTool({ searchTerms })`. */
197
197
  searchTerms?: (container: string) => readonly string[] | undefined;
198
+ /** The behaviour revision of the whole catalog; raising it changes every tool version. */
199
+ revision?: number;
200
+ /** A behaviour revision for a container the host cannot decorate, below a `@McpTool({ revision })`. */
201
+ containerRevision?: (container: string) => number | undefined;
198
202
  schema?: TypeShapeBinderOptions;
199
203
  resourceServer?: SezzleeResourceServerOptions;
200
204
  }
package/dist/options.js CHANGED
@@ -145,6 +145,10 @@ export class SezzleeOptions {
145
145
  tags;
146
146
  /** Search vocabulary for a container the host cannot decorate, below a `@McpTool({ searchTerms })`. */
147
147
  searchTerms;
148
+ /** The behaviour revision of the whole catalog; raising it changes every tool version. */
149
+ revision;
150
+ /** A behaviour revision for a container the host cannot decorate, below a `@McpTool({ revision })`. */
151
+ containerRevision;
148
152
  schema;
149
153
  resourceServer;
150
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sezzlee/sdk-nestjs",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Embeds an MCP layer into an existing NestJS backend: search-first tool discovery over your real endpoints, dispatched through your own guards, pipes and interceptors.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -41,7 +41,7 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@sezzlee/core": "^0.2.0"
44
+ "@sezzlee/core": "^0.2.1"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@modelcontextprotocol/express": ">=2.0.0",