@sezzlee/sdk-nestjs 0.0.0-stage → 0.2.0

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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +360 -2
  3. package/dist/cache.d.ts +18 -0
  4. package/dist/cache.js +55 -0
  5. package/dist/catalog.d.ts +100 -0
  6. package/dist/catalog.js +456 -0
  7. package/dist/decorators.d.ts +172 -0
  8. package/dist/decorators.js +79 -0
  9. package/dist/discovery/body-encoding.d.ts +24 -0
  10. package/dist/discovery/body-encoding.js +211 -0
  11. package/dist/discovery/diagnostics.d.ts +4 -0
  12. package/dist/discovery/diagnostics.js +49 -0
  13. package/dist/discovery/endpoint-discovery.d.ts +52 -0
  14. package/dist/discovery/endpoint-discovery.js +722 -0
  15. package/dist/discovery/type-shape.d.ts +21 -0
  16. package/dist/discovery/type-shape.js +297 -0
  17. package/dist/dispatcher.d.ts +29 -0
  18. package/dist/dispatcher.js +182 -0
  19. package/dist/extension-points.d.ts +34 -0
  20. package/dist/extension-points.js +35 -0
  21. package/dist/families.d.ts +77 -0
  22. package/dist/families.js +157 -0
  23. package/dist/files.d.ts +43 -0
  24. package/dist/files.js +13 -0
  25. package/dist/index.d.ts +48 -0
  26. package/dist/index.js +26 -0
  27. package/dist/invoke-result-mapper.d.ts +16 -0
  28. package/dist/invoke-result-mapper.js +44 -0
  29. package/dist/markers.d.ts +5 -0
  30. package/dist/markers.js +27 -0
  31. package/dist/meta-tools.d.ts +18 -0
  32. package/dist/meta-tools.js +306 -0
  33. package/dist/options-validation.d.ts +17 -0
  34. package/dist/options-validation.js +79 -0
  35. package/dist/options.d.ts +201 -0
  36. package/dist/options.js +151 -0
  37. package/dist/outer-connection.d.ts +18 -0
  38. package/dist/outer-connection.js +23 -0
  39. package/dist/sezzlee.module.d.ts +16 -0
  40. package/dist/sezzlee.module.js +180 -0
  41. package/dist/synthetic-context.d.ts +18 -0
  42. package/dist/synthetic-context.js +131 -0
  43. package/dist/transport/audience.d.ts +2 -0
  44. package/dist/transport/audience.js +21 -0
  45. package/dist/transport/protected-resource-metadata.d.ts +6 -0
  46. package/dist/transport/protected-resource-metadata.js +23 -0
  47. package/dist/transport/streamable-http.d.ts +25 -0
  48. package/dist/transport/streamable-http.js +52 -0
  49. package/dist/visibility/evaluator.d.ts +8 -0
  50. package/dist/visibility/evaluator.js +5 -0
  51. package/dist/visibility/probe.d.ts +25 -0
  52. package/dist/visibility/probe.js +104 -0
  53. package/dist/visibility/provider.d.ts +23 -0
  54. package/dist/visibility/provider.js +118 -0
  55. package/package.json +99 -3
@@ -0,0 +1,456 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { Inject, Injectable, Optional, RequestMethod, } from "@nestjs/common";
14
+ import { ApplicationConfig, DiscoveryService, HttpAdapterHost, ModulesContainer, } from "@nestjs/core";
15
+ import { assertCatalogValid, buildCatalog, combineMarkers, matchesRoute, SezzleeTemplateError, textMediaType, urlEncodedMediaType, } from "@sezzlee/core";
16
+ import { severityOf } from "./discovery/diagnostics.js";
17
+ import { createRoutePaths, discoverEndpoints, modulePathOf, normalizeRoute, } from "./discovery/endpoint-discovery.js";
18
+ import { loadFamilies, resolveFamily, } from "./families.js";
19
+ import { SEZZLEE_OPTIONS, } from "./options.js";
20
+ import { extensionTokens } from "./extension-points.js";
21
+ import { protectedResourceMetadataPath } from "./transport/protected-resource-metadata.js";
22
+ export { cleanTags } from "@sezzlee/core";
23
+ let SezzleeCatalog = class SezzleeCatalog {
24
+ discovery;
25
+ modules;
26
+ applicationConfig;
27
+ options;
28
+ adapterHost;
29
+ toolRanker;
30
+ snapshot;
31
+ currentGeneration = 0;
32
+ listeners = new Set();
33
+ loads;
34
+ pending = Promise.resolve();
35
+ constructor(discovery, modules, applicationConfig, options, adapterHost, toolRanker = null) {
36
+ this.discovery = discovery;
37
+ this.modules = modules;
38
+ this.applicationConfig = applicationConfig;
39
+ this.options = options;
40
+ this.adapterHost = adapterHost;
41
+ this.toolRanker = toolRanker;
42
+ }
43
+ get ranker() {
44
+ return this.toolRanker;
45
+ }
46
+ get generation() {
47
+ return this.currentGeneration;
48
+ }
49
+ get current() {
50
+ this.snapshot ??= this.build(this.loads);
51
+ return this.snapshot;
52
+ }
53
+ onChange(listener) {
54
+ this.listeners.add(listener);
55
+ return () => this.listeners.delete(listener);
56
+ }
57
+ /**
58
+ * Rebuilds the catalog, loading family members first when any source is registered.
59
+ *
60
+ * Without a source the rebuild happens before this returns, as it always has. With one, reloads
61
+ * run one at a time, and a rebuild that would turn a valid catalog fatal is refused and the
62
+ * current catalog kept ([tool-families.md] §Membership): member data added under a name another
63
+ * tool already has would otherwise take every tool offline.
64
+ *
65
+ * @throws SezzleeCatalogError through the returned promise when the rebuild is refused
66
+ */
67
+ reload() {
68
+ if (this.options.families.sources.size === 0) {
69
+ this.commit(this.build(this.loads));
70
+ return Promise.resolve();
71
+ }
72
+ return this.enqueue(async () => {
73
+ const loads = await loadFamilies(this.options.families, this.loads);
74
+ const candidate = this.build(loads);
75
+ if (candidate.fatal.length > 0 && this.current.fatal.length === 0) {
76
+ assertCatalogValid(candidate.fatal);
77
+ }
78
+ this.loads = loads;
79
+ this.commit(candidate);
80
+ });
81
+ }
82
+ /**
83
+ * Loads family members once the application's own providers are ready, since a source usually
84
+ * reads from one of them. A catalog built earlier carries `family_not_loaded` and is rebuilt
85
+ * here, which signals `listChanged`.
86
+ */
87
+ async onApplicationBootstrap() {
88
+ if (this.options.families.sources.size === 0) {
89
+ return;
90
+ }
91
+ await this.enqueue(async () => {
92
+ this.loads = await loadFamilies(this.options.families, this.loads);
93
+ if (this.snapshot !== undefined) {
94
+ this.commit(this.build(this.loads));
95
+ }
96
+ });
97
+ }
98
+ enqueue(task) {
99
+ const next = this.pending.then(task);
100
+ this.pending = next.catch(() => undefined);
101
+ return next;
102
+ }
103
+ commit(snapshot) {
104
+ this.snapshot = snapshot;
105
+ this.currentGeneration += 1;
106
+ for (const listener of this.listeners) {
107
+ listener();
108
+ }
109
+ }
110
+ get diagnostics() {
111
+ return this.current.diagnostics;
112
+ }
113
+ find(name) {
114
+ return this.current.byName.get(name);
115
+ }
116
+ ensureValid() {
117
+ assertCatalogValid(this.current.fatal);
118
+ }
119
+ /**
120
+ * Guard: Express 5 defaults `query parser` to `simple`, which does not parse
121
+ * brackets — `?filter[status]=x` then arrives as one literal key and the DTO
122
+ * binds nothing. A tool that composes a filter the backend silently ignores
123
+ * returns an unfiltered result set, which is the failure
124
+ * `unresolved_query_shape` exists to prevent, so this is fatal rather than a
125
+ * warning. Only Express is checked: `getInstance().get` is a route
126
+ * registrar on Fastify, not a settings reader.
127
+ */
128
+ reportUnparsedBrackets(entries, report) {
129
+ const bracketed = entries.filter((entry) => (entry.descriptor.parameters ?? []).some((parameter) => parameter.style === "deepObject" &&
130
+ (parameter.objectNotation ?? "bracket") === "bracket"));
131
+ if (bracketed.length === 0) {
132
+ return;
133
+ }
134
+ const adapter = this.adapterHost?.httpAdapter;
135
+ if (adapter === undefined || adapter.getType() !== "express") {
136
+ return;
137
+ }
138
+ const instance = adapter.getInstance();
139
+ const parser = instance.get("query parser");
140
+ if (parser === "extended" || typeof parser === "function") {
141
+ return;
142
+ }
143
+ report({
144
+ code: "query_parser_not_extended",
145
+ message: `Tool '${bracketed[0]?.tool.name}' composes a bracketed query object, but this application's Express 'query parser' is '${String(parser)}', which delivers one literal key instead of an object. Call app.set('query parser', 'extended'), or declare objectNotation on the parameter.`,
146
+ });
147
+ }
148
+ /**
149
+ * Guard: Express parses a body only for the media types a registered parser declares, and an
150
+ * unparsed body reaches the handler as `req.body === undefined` with no error (form-body-probe
151
+ * N6) — a tool the backend answers as if every field were absent. The default parsers are
152
+ * `jsonParser` and `urlencodedParser`, visible by name on the router stack (N7); a text body
153
+ * needs `app.useBodyParser('text')`. Only Express is checked, for `reportUnparsedBrackets`'s
154
+ * reason.
155
+ */
156
+ lacksBodyParser(descriptor) {
157
+ const contentType = descriptor.requestBody?.contentType;
158
+ const needed = contentType === textMediaType
159
+ ? "textParser"
160
+ : contentType === urlEncodedMediaType
161
+ ? "urlencodedParser"
162
+ : undefined;
163
+ const adapter = this.adapterHost?.httpAdapter;
164
+ if (needed === undefined ||
165
+ adapter === undefined ||
166
+ adapter.getType() !== "express") {
167
+ return undefined;
168
+ }
169
+ const instance = adapter.getInstance();
170
+ const stack = instance.router?.stack ?? instance._router?.stack ?? [];
171
+ return stack.some((layer) => layer.name === needed) ? undefined : needed;
172
+ }
173
+ build(loads) {
174
+ const prior = [];
175
+ const report = (diagnostic) => {
176
+ prior.push(diagnostic);
177
+ };
178
+ const routePaths = createRoutePaths(this.applicationConfig);
179
+ const globalPrefix = this.applicationConfig.getGlobalPrefix();
180
+ const versioningOptions = this.applicationConfig.getVersioning();
181
+ const mcpPath = this.options.resourceServer?.mcpPath ?? "/mcp";
182
+ this.checkMetadataPath(globalPrefix, mcpPath, routePaths, report);
183
+ const refDescription = this.options.files.resolver?.refDescription;
184
+ const files = refDescription === undefined ? undefined : { refDescription };
185
+ const applicationId = this.modules.applicationId;
186
+ const controllers = this.discovery
187
+ .getControllers()
188
+ .filter((wrapper) => typeof wrapper.metatype === "function")
189
+ .map((wrapper) => {
190
+ const modulePath = modulePathOf(wrapper.host?.metatype, applicationId);
191
+ return {
192
+ metatype: wrapper.metatype,
193
+ ...(modulePath === undefined ? {} : { modulePath }),
194
+ };
195
+ });
196
+ const discovered = discoverEndpoints(controllers, {
197
+ ...(this.options.schema === undefined
198
+ ? {}
199
+ : { schema: this.options.schema }),
200
+ ...(versioningOptions === undefined ? {} : { versioningOptions }),
201
+ globalPrefix,
202
+ routePaths,
203
+ severity: (code) => severityOf(code, this.options.diagnostics),
204
+ queryGrouping: this.options.query.grouping,
205
+ report,
206
+ });
207
+ const reserved = routePaths
208
+ .create({ globalPrefix, methodPath: mcpPath }, RequestMethod.ALL)
209
+ .map(normalizeRoute);
210
+ const families = new Map();
211
+ const routed = discovered.filter((endpoint) => {
212
+ if (reserved.some((path) => endpoint.descriptor.route.startsWith(path))) {
213
+ return false;
214
+ }
215
+ const family = endpoint.hints.family;
216
+ if (family === undefined) {
217
+ return true;
218
+ }
219
+ const resolution = resolveFamily(family, `${endpoint.controller.name}.${endpoint.handlerName}`, endpoint.hints.variants ?? [], this.options.families, loads);
220
+ if (resolution.kind === "refused") {
221
+ report(resolution.diagnostic);
222
+ return false;
223
+ }
224
+ if (resolution.kind === "members") {
225
+ resolution.diagnostics.forEach(report);
226
+ families.set(endpoint, resolution);
227
+ }
228
+ return true;
229
+ });
230
+ 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);
234
+ return {
235
+ source: {
236
+ controller: endpoint.controller,
237
+ handlerName: endpoint.handlerName,
238
+ },
239
+ owner: `${endpoint.controller.name}.${endpoint.handlerName}`,
240
+ descriptor: endpoint.descriptor,
241
+ ...markersOf(endpoint),
242
+ ...(tags === undefined ? {} : { tags }),
243
+ ...(searchTerms === undefined ? {} : { searchTerms }),
244
+ declare: (cleaned) => declaredDescriptor(endpoint, this.curationFor(endpoint), cleaned, families.get(endpoint)),
245
+ };
246
+ }), {
247
+ selection: this.options.selection,
248
+ prefixMode: this.options.naming.prefixMode,
249
+ ...(files === undefined ? {} : { files }),
250
+ providers: this.options.arguments.providers,
251
+ severity: (code) => severityOf(code, this.options.diagnostics),
252
+ failOn: this.options.diagnostics.failOn ?? "fatal",
253
+ prior,
254
+ admit: (descriptor, name) => {
255
+ const missingParser = this.lacksBodyParser(descriptor);
256
+ return missingParser === undefined
257
+ ? undefined
258
+ : {
259
+ code: "body_parser_missing",
260
+ message: `Tool '${name}' sends a ${descriptor.requestBody?.contentType ?? ""} body, but this application registers no ${missingParser}; the handler would receive no body. Register the parser with app.useBodyParser(), or declare another media type.`,
261
+ };
262
+ },
263
+ inspect: (entries, inspectReport) => this.reportUnparsedBrackets(entries, inspectReport),
264
+ });
265
+ return { ...built, discovered: discovered.length };
266
+ }
267
+ checkMetadataPath(globalPrefix, mcpPath, routePaths, report) {
268
+ if (this.options.resourceServer === undefined || globalPrefix === "") {
269
+ return;
270
+ }
271
+ const metadataPath = protectedResourceMetadataPath(mcpPath);
272
+ const [served] = routePaths.create({ globalPrefix, methodPath: metadataPath }, RequestMethod.GET);
273
+ if (served === metadataPath) {
274
+ return;
275
+ }
276
+ report({
277
+ code: "prm_path_prefixed",
278
+ message: `The global prefix moves the protected-resource metadata to '${String(served)}', but RFC 9728 requires it at '${metadataPath}'; clients cannot discover the authorization server. Pass it to setGlobalPrefix's exclude list: setGlobalPrefix('${globalPrefix}', { exclude: ['${metadataPath}'] }).`,
279
+ });
280
+ }
281
+ /**
282
+ * Applies the declaration ladder, least specific first, with sealed fields last.
283
+ *
284
+ * Merging is per argument and per field, so a global rule that hides a tenant identifier and a
285
+ * method-level rule that renames a page number both survive. A decorator wins a tie against a
286
+ * central rule of the same specificity: it is nearer the code and that is the reading a
287
+ * maintainer expects.
288
+ */
289
+ curationFor(endpoint) {
290
+ const central = this.options.arguments.rules
291
+ .filter((rule) => matchesTarget(rule, endpoint))
292
+ .sort((a, b) => a.specificity - b.specificity);
293
+ assertUnambiguous(central, endpoint);
294
+ const layers = [
295
+ ...central.filter((rule) => !rule.sealed),
296
+ { rules: endpoint.hints.arguments ?? {}, sealed: false },
297
+ ...central.filter((rule) => rule.sealed),
298
+ ];
299
+ /**
300
+ * Collected before the merge, not during it: sealed rules are applied last so that they win,
301
+ * which means a check that learns the sealed names as it goes can never see an override.
302
+ */
303
+ const sealed = new Set(central
304
+ .filter((rule) => rule.sealed)
305
+ .flatMap((rule) => Object.keys(rule.rules)));
306
+ const merged = new Map();
307
+ for (const layer of layers) {
308
+ for (const [name, rule] of Object.entries(layer.rules)) {
309
+ if (sealed.has(name) && !layer.sealed) {
310
+ throw new SezzleeTemplateError("sealed_curation_overridden", `Argument '${name}' is sealed on ${endpoint.controller.name}.${endpoint.handlerName}; a sealed rule cannot be overridden.`);
311
+ }
312
+ merged.set(name, rule);
313
+ }
314
+ }
315
+ return toCuration(Object.fromEntries(merged));
316
+ }
317
+ };
318
+ SezzleeCatalog = __decorate([
319
+ Injectable(),
320
+ __param(3, Inject(SEZZLEE_OPTIONS)),
321
+ __param(4, Optional()),
322
+ __param(5, Optional()),
323
+ __param(5, Inject(extensionTokens.toolRanker)),
324
+ __metadata("design:paramtypes", [DiscoveryService,
325
+ ModulesContainer,
326
+ ApplicationConfig, Function, HttpAdapterHost, Object])
327
+ ], SezzleeCatalog);
328
+ export { SezzleeCatalog };
329
+ function markersOf(endpoint) {
330
+ const container = combineMarkers(endpoint.containerMarkers.includes(true), endpoint.containerMarkers.includes(false));
331
+ const operation = combineMarkers(endpoint.operationMarkers.includes(true), endpoint.operationMarkers.includes(false));
332
+ return {
333
+ ...(container === undefined ? {} : { container }),
334
+ ...(operation === undefined ? {} : { operation }),
335
+ };
336
+ }
337
+ /**
338
+ * Rejects two central rules that set the same argument differently at the same specificity.
339
+ *
340
+ * Between levels the nearer rule simply wins, silently and by design. Within one level there is no
341
+ * nearer rule, so a merge would have to pick by registration order and the host would be reading a
342
+ * ladder that does not decide anything.
343
+ */
344
+ function assertUnambiguous(rules, endpoint) {
345
+ const claimed = new Map();
346
+ for (const rule of rules) {
347
+ for (const [name, declaration] of Object.entries(rule.rules)) {
348
+ const key = `${rule.specificity}|${rule.sealed ? "sealed" : "open"}|${name}`;
349
+ const written = JSON.stringify(declaration);
350
+ const existing = claimed.get(key);
351
+ if (existing !== undefined && existing !== written) {
352
+ throw new SezzleeTemplateError("ambiguous_curation", `Two curation rules of equal specificity declare argument '${name}' differently on ${endpoint.controller.name}.${endpoint.handlerName}; narrow one of their targets.`);
353
+ }
354
+ claimed.set(key, written);
355
+ }
356
+ }
357
+ }
358
+ /**
359
+ * Folds the decorator's declarations into the descriptor.
360
+ *
361
+ * Discovery reports facts; declarations are reduced here, the same way `toolName` and
362
+ * `containerPrefix` are. The curation list arrives resolved because the ladder that produces it
363
+ * needs the host's central rules, which are not a property of the endpoint; a family's members
364
+ * arrive already lowered, because they were loaded from a source rather than declared.
365
+ */
366
+ export function declaredDescriptor(endpoint, curation, tags, family) {
367
+ const hints = endpoint.hints;
368
+ const annotations = annotationsOf(hints);
369
+ const declaredFamily = family?.family ??
370
+ (hints.family === undefined
371
+ ? undefined
372
+ : { parameter: hints.family.parameter });
373
+ const variants = [
374
+ ...(hints.variants ?? []).map(declaredVariant),
375
+ ...(family?.variants ?? []),
376
+ ];
377
+ return {
378
+ ...endpoint.descriptor,
379
+ ...(hints.name === undefined ? {} : { toolName: hints.name }),
380
+ ...(hints.prefix === undefined ? {} : { containerPrefix: hints.prefix }),
381
+ ...(tags === undefined ? {} : { tags: [...tags] }),
382
+ ...(curation.length === 0 ? {} : { arguments: [...curation] }),
383
+ ...(variants.length === 0
384
+ ? {}
385
+ : { variants: variants }),
386
+ ...(declaredFamily === undefined ? {} : { family: declaredFamily }),
387
+ ...(annotations === undefined ? {} : { annotations }),
388
+ };
389
+ }
390
+ function declaredVariant(variant) {
391
+ const annotations = annotationsOf(variant);
392
+ return {
393
+ name: variant.name,
394
+ description: variant.description,
395
+ ...(variant.arguments === undefined
396
+ ? {}
397
+ : { arguments: toCuration(variant.arguments) }),
398
+ ...(variant.body === undefined
399
+ ? {}
400
+ : {
401
+ requestBody: {
402
+ schema: variant.body,
403
+ ...(variant.bodyRequired === undefined
404
+ ? {}
405
+ : { required: variant.bodyRequired }),
406
+ },
407
+ }),
408
+ ...(annotations === undefined ? {} : { annotations }),
409
+ };
410
+ }
411
+ function annotationsOf(declared) {
412
+ const annotations = {
413
+ ...(declared.readOnly === undefined
414
+ ? {}
415
+ : { readOnlyHint: declared.readOnly }),
416
+ ...(declared.destructive === undefined
417
+ ? {}
418
+ : { destructiveHint: declared.destructive }),
419
+ ...(declared.idempotent === undefined
420
+ ? {}
421
+ : { idempotentHint: declared.idempotent }),
422
+ };
423
+ return Object.keys(annotations).length === 0 ? undefined : annotations;
424
+ }
425
+ export function toCuration(rules) {
426
+ return Object.entries(rules).map(([name, rule]) => ({
427
+ name,
428
+ ...(rule.hide === undefined
429
+ ? {
430
+ ...(rule.as === undefined ? {} : { as: rule.as }),
431
+ ...(rule.description === undefined
432
+ ? {}
433
+ : { description: rule.description }),
434
+ }
435
+ : { hidden: rule.hide }),
436
+ }));
437
+ }
438
+ function matchesTarget(rule, endpoint) {
439
+ const target = rule.target;
440
+ if (target.controller !== undefined &&
441
+ target.controller !== endpoint.controller) {
442
+ return false;
443
+ }
444
+ if (target.handler !== undefined && target.handler !== endpoint.handlerName) {
445
+ return false;
446
+ }
447
+ if (target.method !== undefined &&
448
+ target.method.toUpperCase() !== endpoint.descriptor.method) {
449
+ return false;
450
+ }
451
+ if (target.route !== undefined &&
452
+ !matchesRoute(target.route, endpoint.descriptor.route)) {
453
+ return false;
454
+ }
455
+ return true;
456
+ }
@@ -0,0 +1,172 @@
1
+ import type { ArgumentFill, EndpointDescriptor, JsonSchemaObject } from "@sezzlee/core";
2
+ export type JsonValue = string | number | boolean | null | readonly JsonValue[] | {
3
+ readonly [key: string]: JsonValue;
4
+ };
5
+ /**
6
+ * A curation rule for one argument.
7
+ *
8
+ * The union is what makes "hidden but also renamed" unrepresentable: a hidden argument has no
9
+ * agent-facing name or description to carry.
10
+ */
11
+ export type ArgumentRule = {
12
+ readonly hide?: undefined;
13
+ readonly as?: string;
14
+ readonly description?: string;
15
+ } | {
16
+ readonly hide: ArgumentFill;
17
+ };
18
+ export declare const hidden: {
19
+ readonly value: (value: JsonValue) => ArgumentRule;
20
+ readonly from: (source: string) => ArgumentRule;
21
+ readonly omit: () => ArgumentRule;
22
+ };
23
+ export type ArgumentRules<T> = {
24
+ readonly [K in keyof T & string]?: ArgumentRule;
25
+ };
26
+ /**
27
+ * Key-checks a curation against a DTO's own keys.
28
+ *
29
+ * Optional: `arguments` accepts a plain record. Reaching for this moves the
30
+ * `curation_unresolved` diagnostic from startup to compile time, which is where a typo belongs.
31
+ */
32
+ export declare function curate<T>(rules: ArgumentRules<T>): ArgumentRules<T>;
33
+ type DescriptorParameter = NonNullable<EndpointDescriptor["parameters"]>[number];
34
+ export interface McpParameterOptions {
35
+ readonly required?: boolean;
36
+ readonly style?: DescriptorParameter["style"];
37
+ readonly explode?: boolean;
38
+ }
39
+ /**
40
+ * What one status code returns.
41
+ *
42
+ * A bare class is bound through the same TypeShape reader the request body uses; a one-element
43
+ * tuple declares a collection of it; `{ schema }` supplies a schema verbatim; `{}` declares a
44
+ * status that carries no body.
45
+ */
46
+ export type McpResponseDeclaration = NewableFunction | readonly [NewableFunction] | {
47
+ readonly schema: JsonSchemaObject;
48
+ } | Record<string, never>;
49
+ export interface McpFileFieldOptions {
50
+ /** A `FilesInterceptor` field, sent as repeated parts. */
51
+ readonly multiple?: boolean;
52
+ readonly required?: boolean;
53
+ readonly description?: string;
54
+ /** The media type the backend expects; it becomes the part's default. */
55
+ readonly mediaType?: string;
56
+ }
57
+ export interface McpToolOptions {
58
+ readonly name?: string;
59
+ readonly prefix?: string;
60
+ readonly description?: string;
61
+ readonly body?: JsonSchemaObject;
62
+ /**
63
+ * What the endpoint returns, keyed by status code.
64
+ *
65
+ * A declaration is needed because a handler's return type is not readable at runtime: TypeScript
66
+ * emits `design:returntype` with the generic erased, so an `async` handler reports `Promise` and
67
+ * a collection reports `Array`. Discovery falls back to `@nestjs/swagger`'s response metadata and
68
+ * then to `design:returntype`, but only a declaration here is guaranteed to be exact.
69
+ */
70
+ readonly responses?: Readonly<Record<string, McpResponseDeclaration>>;
71
+ /**
72
+ * Whether the backend requires a body at all, as distinct from requiring the
73
+ * fields inside it. `false` makes omitting the body expressible: the agent
74
+ * then sends no body rather than an empty object.
75
+ */
76
+ readonly bodyRequired?: boolean;
77
+ /**
78
+ * Per-parameter declarations keyed by parameter name. Nest exposes no runtime
79
+ * signal that proves a named `@Query`/`@Headers` is required, nor how an
80
+ * array-valued one is serialised, so both are declarations rather than
81
+ * inferences.
82
+ */
83
+ readonly parameters?: Readonly<Record<string, McpParameterOptions>>;
84
+ /**
85
+ * Curation declarations keyed by wire name: a parameter, a flattened body field, or the body
86
+ * root argument. Distinct from `parameters`, which corrects a discovered fact; these declare the
87
+ * agent-facing surface and span the body too.
88
+ */
89
+ readonly arguments?: Readonly<Record<string, ArgumentRule>>;
90
+ /**
91
+ * The operation's grouping labels, replacing the one derived from the container rather than
92
+ * adding to it, so a host can remove a grouping it did not choose. They are `search_tools`
93
+ * filter keys and search index vocabulary at once ([search-semantics.md]).
94
+ */
95
+ readonly tags?: readonly string[];
96
+ /**
97
+ * Search vocabulary the operation's own text does not contain. Indexed like the description,
98
+ * never a filter key and never shown to the agent ([search-semantics.md]).
99
+ */
100
+ readonly searchTerms?: readonly string[];
101
+ /**
102
+ * The body's media type, replacing the one discovery chooses. It is the host's own claim that a
103
+ * parser for it is registered; when `@ApiConsumes` also lists types, it must be one of them.
104
+ */
105
+ readonly consumes?: string;
106
+ /**
107
+ * The multipart file fields, keyed by the name the file interceptor reads. `FileInterceptor`
108
+ * keeps that name in a closure, so without a declaration here or an `@ApiBody` schema marking
109
+ * the field `format: "binary"` the endpoint is dropped rather than guessed at.
110
+ */
111
+ readonly files?: Readonly<Record<string, McpFileFieldOptions>>;
112
+ readonly readOnly?: boolean;
113
+ readonly destructive?: boolean;
114
+ readonly idempotent?: boolean;
115
+ readonly variants?: readonly McpVariantOptions[];
116
+ readonly family?: McpToolFamilyOptions;
117
+ }
118
+ /**
119
+ * The operation dispatches on one parameter, and each of its variants is a member that writes its
120
+ * own key there ([tool-families.md]).
121
+ *
122
+ * @param parameter the dispatch parameter's wire name
123
+ * @param source the name a member source was registered under with `options.families.provide`;
124
+ * without one the members are the handler's `@McpVariant`s alone
125
+ */
126
+ export interface McpToolFamilyOptions {
127
+ readonly parameter: string;
128
+ readonly source?: string;
129
+ }
130
+ /**
131
+ * Behaviour hints a member declares. The union keeps a read-only member from also claiming to be
132
+ * destructive, which MCP defines as meaningless.
133
+ */
134
+ export type McpToolEffect = {
135
+ readonly readOnly: true;
136
+ readonly destructive?: never;
137
+ readonly idempotent?: boolean;
138
+ } | {
139
+ readonly readOnly?: false;
140
+ readonly destructive?: boolean;
141
+ readonly idempotent?: boolean;
142
+ };
143
+ /**
144
+ * One of several tools produced from a single operation.
145
+ *
146
+ * `name` and `description` are non-optional because one description cannot honestly describe two
147
+ * tools whose arguments are hidden differently; `tsc` therefore enforces the rule at the
148
+ * declaration site.
149
+ */
150
+ export interface McpVariantOptions {
151
+ readonly name: string;
152
+ readonly description: string;
153
+ readonly arguments?: Readonly<Record<string, ArgumentRule>>;
154
+ /** A family member's own body; refused on a variant of an operation that is not a family. */
155
+ readonly body?: JsonSchemaObject;
156
+ readonly bodyRequired?: boolean;
157
+ readonly readOnly?: boolean;
158
+ readonly destructive?: boolean;
159
+ readonly idempotent?: boolean;
160
+ }
161
+ export interface McpSelectionMarker {
162
+ readonly include: boolean;
163
+ readonly options: McpToolOptions;
164
+ }
165
+ export declare const MCP_SELECTION = "sezzlee:selection";
166
+ export declare function McpVariant(options: McpVariantOptions): MethodDecorator;
167
+ export declare function McpToolFamily(options: McpToolFamilyOptions): MethodDecorator;
168
+ type Target = object | ((...args: never[]) => unknown);
169
+ export declare function McpTool(options?: McpToolOptions): ClassDecorator & MethodDecorator;
170
+ export declare function McpIgnore(): ClassDecorator & MethodDecorator;
171
+ export declare function markersOf(target: Target | undefined): McpSelectionMarker[];
172
+ export {};