@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,151 @@
1
+ import { defaultRankerTimeoutMs, invokeLimits } from "@sezzlee/core";
2
+ import { ToolFamilyOptions } from "./families.js";
3
+ export function callerOf(outer) {
4
+ const auth = outer?.auth;
5
+ const extra = auth?.extra ?? {};
6
+ const subject = typeof extra["sub"] === "string" ? extra["sub"] : auth?.clientId;
7
+ return {
8
+ ...(subject === undefined ? {} : { subject }),
9
+ ...(auth?.clientId === undefined ? {} : { clientId: auth.clientId }),
10
+ scopes: auth?.scopes ?? [],
11
+ ...(auth?.resource === undefined
12
+ ? {}
13
+ : { resource: auth.resource.toString() }),
14
+ ...(auth?.expiresAt === undefined
15
+ ? {}
16
+ : { expiresAt: new Date(auth.expiresAt * 1000) }),
17
+ claim: (name) => {
18
+ const value = extra[name];
19
+ return typeof value === "string" ? value : undefined;
20
+ },
21
+ header: (name) => {
22
+ const value = outer?.headers[name.toLowerCase()];
23
+ return Array.isArray(value) ? value.join(", ") : value;
24
+ },
25
+ };
26
+ }
27
+ function specificityOf(target) {
28
+ if (target.handler !== undefined) {
29
+ return 3;
30
+ }
31
+ if (target.controller !== undefined || target.route !== undefined) {
32
+ return 2;
33
+ }
34
+ return 1;
35
+ }
36
+ /**
37
+ * Central curation: rules for controllers the host cannot decorate, and rules that repeat.
38
+ *
39
+ * `seal` exists because plain "most specific wins" lets a method decorator defeat a tenancy rule.
40
+ * A sealed field cannot be overridden by any layer; attempting it is a build error.
41
+ */
42
+ export class ArgumentCurationOptions {
43
+ valueProviders = new Map();
44
+ declarations = [];
45
+ get providers() {
46
+ return this.valueProviders;
47
+ }
48
+ get rules() {
49
+ return this.declarations;
50
+ }
51
+ provide(name, provider) {
52
+ this.valueProviders.set(name, provider);
53
+ return this;
54
+ }
55
+ curate(target, rules) {
56
+ this.declarations.push({
57
+ target,
58
+ rules,
59
+ sealed: false,
60
+ specificity: specificityOf(target),
61
+ });
62
+ return this;
63
+ }
64
+ everywhere(rules) {
65
+ return this.curate({}, rules);
66
+ }
67
+ seal(target, rules) {
68
+ this.declarations.push({
69
+ target,
70
+ rules,
71
+ sealed: true,
72
+ specificity: specificityOf(target),
73
+ });
74
+ return this;
75
+ }
76
+ }
77
+ export class IdentityForwardingOptions {
78
+ names = new Set(["authorization"]);
79
+ projector;
80
+ get carriers() {
81
+ return this.names;
82
+ }
83
+ forward(name) {
84
+ this.names.add(name.toLowerCase());
85
+ return this;
86
+ }
87
+ clear() {
88
+ this.names.clear();
89
+ return this;
90
+ }
91
+ project(projector) {
92
+ this.projector = projector;
93
+ return this;
94
+ }
95
+ }
96
+ export class ErrorMappingOptions {
97
+ items = [];
98
+ get recognizers() {
99
+ return this.items;
100
+ }
101
+ recognize(recognizer) {
102
+ this.items.push(recognizer);
103
+ return this;
104
+ }
105
+ }
106
+ export class SezzleeOptions {
107
+ identity = new IdentityForwardingOptions();
108
+ synthetic = { accept: "application/json" };
109
+ cache = { lifetimeMs: 30_000, maxCallers: 128 };
110
+ errors = new ErrorMappingOptions();
111
+ selection = { default: "exclude" };
112
+ query = { grouping: "flatten" };
113
+ naming = { prefixMode: "always" };
114
+ diagnostics = {
115
+ failOn: "fatal",
116
+ escalate: new Set(),
117
+ downgrade: new Set(),
118
+ };
119
+ arguments = new ArgumentCurationOptions();
120
+ families = new ToolFamilyOptions();
121
+ visibility = {
122
+ tier: "declarative",
123
+ onUnknown: "show",
124
+ probeTopK: 25,
125
+ probeConcurrency: 4,
126
+ probeValues: new Map(),
127
+ };
128
+ invoke = {
129
+ maxResponseBytes: invokeLimits.maxResponseBytes,
130
+ timeoutMs: invokeLimits.invokeTimeoutMs,
131
+ maxInlineFileBytes: invokeLimits.maxInlineFileBytes,
132
+ maxFileBytes: invokeLimits.maxFileBytes,
133
+ };
134
+ search = {
135
+ rankerTimeoutMs: defaultRankerTimeoutMs,
136
+ onRankerFailure: "fallback",
137
+ };
138
+ /** Binding `files.resolver` is what makes `ref` appear in a file argument's schema. */
139
+ files = {};
140
+ /**
141
+ * Grouping labels for a container the host cannot decorate. It sits below a `@McpTool({ tags })`
142
+ * declaration and above the container-derived default, and like a declaration it replaces that
143
+ * default rather than adding to it.
144
+ */
145
+ tags;
146
+ /** Search vocabulary for a container the host cannot decorate, below a `@McpTool({ searchTerms })`. */
147
+ searchTerms;
148
+ schema;
149
+ resourceServer;
150
+ }
151
+ export const SEZZLEE_OPTIONS = "SEZZLEE_OPTIONS";
@@ -0,0 +1,18 @@
1
+ export interface OuterConnection {
2
+ readonly remoteAddress?: string;
3
+ readonly remotePort?: number;
4
+ readonly localAddress?: string;
5
+ readonly localPort?: number;
6
+ }
7
+ interface SocketLike {
8
+ readonly remoteAddress?: string;
9
+ readonly remotePort?: number;
10
+ readonly localAddress?: string;
11
+ readonly localPort?: number;
12
+ }
13
+ export declare function connectionOf(source: {
14
+ readonly socket?: SocketLike;
15
+ } | undefined): OuterConnection | undefined;
16
+ export declare function runWithOuterConnection<T>(connection: OuterConnection | undefined, fn: () => T): T;
17
+ export declare function currentOuterConnection(): OuterConnection | undefined;
18
+ export {};
@@ -0,0 +1,23 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const storage = new AsyncLocalStorage();
3
+ export function connectionOf(source) {
4
+ const socket = source?.socket;
5
+ if (socket === undefined) {
6
+ return undefined;
7
+ }
8
+ const connection = {
9
+ remoteAddress: socket.remoteAddress,
10
+ remotePort: socket.remotePort,
11
+ localAddress: socket.localAddress,
12
+ localPort: socket.localPort,
13
+ };
14
+ return Object.values(connection).some((value) => value !== undefined)
15
+ ? connection
16
+ : undefined;
17
+ }
18
+ export function runWithOuterConnection(connection, fn) {
19
+ return connection === undefined ? fn() : storage.run(connection, fn);
20
+ }
21
+ export function currentOuterConnection() {
22
+ return storage.getStore();
23
+ }
@@ -0,0 +1,16 @@
1
+ import { type DynamicModule, type InjectionToken, type MiddlewareConsumer, type NestModule } from "@nestjs/common";
2
+ import { type ExtensionOverrides } from "./extension-points.js";
3
+ import { SezzleeOptions } from "./options.js";
4
+ export interface SezzleeModuleAsyncOptions {
5
+ imports?: DynamicModule["imports"];
6
+ inject?: InjectionToken[];
7
+ useFactory: (...args: never[]) => SezzleeOptions | Promise<SezzleeOptions>;
8
+ overrides?: ExtensionOverrides;
9
+ }
10
+ export declare class SezzleeModule implements NestModule {
11
+ private readonly options;
12
+ constructor(options: SezzleeOptions);
13
+ static forRoot(configure?: (options: SezzleeOptions) => void, overrides?: ExtensionOverrides): DynamicModule;
14
+ static forRootAsync(asyncOptions: SezzleeModuleAsyncOptions): DynamicModule;
15
+ configure(consumer: MiddlewareConsumer): void;
16
+ }
@@ -0,0 +1,180 @@
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
+ var SezzleeModule_1;
14
+ import { Global, Inject, Module, RequestMethod, } from "@nestjs/common";
15
+ import { APP_INTERCEPTOR, DiscoveryModule } from "@nestjs/core";
16
+ import { requireBearerAuth } from "@modelcontextprotocol/express";
17
+ import { MemorySezzleeCache } from "@sezzlee/core";
18
+ import { SezzleeCatalog } from "./catalog.js";
19
+ const SEZZLEE_PROBE_RESET = Symbol("SEZZLEE_PROBE_RESET");
20
+ const SEZZLEE_LIST_CHANGED = Symbol("SEZZLEE_LIST_CHANGED");
21
+ import { DeclarativeVisibilityEvaluator } from "./visibility/evaluator.js";
22
+ import { SezzleeProbeEvaluator, SezzleeProbeInterceptor, } from "./visibility/probe.js";
23
+ import { CallerVisibilityProvider } from "./visibility/provider.js";
24
+ import { CarrierHashCallerScopeResolver, SEZZLEE_CACHE_INVALIDATOR, SezzleeCacheInvalidator, } from "./cache.js";
25
+ import { SezzleeDispatcher } from "./dispatcher.js";
26
+ import { extensionTokens, toProviders, } from "./extension-points.js";
27
+ import { DefaultInvokeResultMapper } from "./invoke-result-mapper.js";
28
+ import { SEZZLEE_OPTIONS, SezzleeOptions } from "./options.js";
29
+ import { validateSezzleeOptions } from "./options-validation.js";
30
+ import { withAudienceCheck } from "./transport/audience.js";
31
+ import { protectedResourceMetadataHandler, protectedResourceMetadataPath, protectedResourceMetadataUrl, } from "./transport/protected-resource-metadata.js";
32
+ import { SezzleeStreamableHttp } from "./transport/streamable-http.js";
33
+ function defaultProviders() {
34
+ return [
35
+ {
36
+ provide: extensionTokens.cache,
37
+ useFactory: (options) => new MemorySezzleeCache({
38
+ lifetimeMs: options.cache.lifetimeMs,
39
+ maxCallers: options.cache.maxCallers,
40
+ }),
41
+ inject: [SEZZLEE_OPTIONS],
42
+ },
43
+ {
44
+ provide: extensionTokens.callerScopeResolver,
45
+ useClass: CarrierHashCallerScopeResolver,
46
+ },
47
+ {
48
+ provide: extensionTokens.invokeResultMapper,
49
+ useClass: DefaultInvokeResultMapper,
50
+ },
51
+ {
52
+ provide: extensionTokens.visibilityEvaluator,
53
+ useClass: DeclarativeVisibilityEvaluator,
54
+ },
55
+ { provide: extensionTokens.toolRanker, useValue: null },
56
+ {
57
+ provide: extensionTokens.probeEvaluator,
58
+ useFactory: (dispatcher, options) => new SezzleeProbeEvaluator(dispatcher, options),
59
+ inject: [SezzleeDispatcher, SEZZLEE_OPTIONS],
60
+ },
61
+ {
62
+ provide: SEZZLEE_PROBE_RESET,
63
+ useFactory: (catalog, prober) => catalog.onChange(() => {
64
+ if (prober instanceof SezzleeProbeEvaluator) {
65
+ prober.clearDisabled();
66
+ }
67
+ }),
68
+ inject: [SezzleeCatalog, extensionTokens.probeEvaluator],
69
+ },
70
+ {
71
+ provide: CallerVisibilityProvider,
72
+ useFactory: (evaluator, prober, cache, options) => new CallerVisibilityProvider(evaluator, prober, cache, options),
73
+ inject: [
74
+ extensionTokens.visibilityEvaluator,
75
+ extensionTokens.probeEvaluator,
76
+ extensionTokens.cache,
77
+ SEZZLEE_OPTIONS,
78
+ ],
79
+ },
80
+ {
81
+ /**
82
+ * Guard: the 2026-07-28 revision delivers `tools/list_changed` only on a
83
+ * `subscriptions/listen` stream the client opened, so the notification has to be published on
84
+ * the handler's bus. It cannot be bound per session any more — there are no sessions, and the
85
+ * per-request server the handler builds is gone before the next catalogue reload.
86
+ */
87
+ provide: SEZZLEE_LIST_CHANGED,
88
+ useFactory: (catalog, transport) => catalog.onChange(() => {
89
+ transport.notifyToolListChanged();
90
+ }),
91
+ inject: [SezzleeCatalog, SezzleeStreamableHttp],
92
+ },
93
+ { provide: APP_INTERCEPTOR, useClass: SezzleeProbeInterceptor },
94
+ SezzleeCatalog,
95
+ SezzleeDispatcher,
96
+ SezzleeStreamableHttp,
97
+ SezzleeCacheInvalidator,
98
+ {
99
+ provide: SEZZLEE_CACHE_INVALIDATOR,
100
+ useExisting: SezzleeCacheInvalidator,
101
+ },
102
+ ];
103
+ }
104
+ function moduleExports() {
105
+ return [
106
+ ...Object.values(extensionTokens),
107
+ CallerVisibilityProvider,
108
+ SezzleeCatalog,
109
+ SezzleeDispatcher,
110
+ SezzleeStreamableHttp,
111
+ SezzleeCacheInvalidator,
112
+ SEZZLEE_CACHE_INVALIDATOR,
113
+ SEZZLEE_OPTIONS,
114
+ ];
115
+ }
116
+ let SezzleeModule = SezzleeModule_1 = class SezzleeModule {
117
+ options;
118
+ constructor(options) {
119
+ this.options = options;
120
+ }
121
+ static forRoot(configure, overrides) {
122
+ const options = new SezzleeOptions();
123
+ configure?.(options);
124
+ validateSezzleeOptions(options);
125
+ return {
126
+ module: SezzleeModule_1,
127
+ imports: [DiscoveryModule],
128
+ providers: [
129
+ { provide: SEZZLEE_OPTIONS, useValue: options },
130
+ ...defaultProviders(),
131
+ ...toProviders(overrides),
132
+ ],
133
+ exports: moduleExports(),
134
+ };
135
+ }
136
+ static forRootAsync(asyncOptions) {
137
+ return {
138
+ module: SezzleeModule_1,
139
+ imports: [DiscoveryModule, ...(asyncOptions.imports ?? [])],
140
+ providers: [
141
+ {
142
+ provide: SEZZLEE_OPTIONS,
143
+ useFactory: async (...args) => {
144
+ const options = await asyncOptions.useFactory(...args);
145
+ validateSezzleeOptions(options);
146
+ return options;
147
+ },
148
+ inject: asyncOptions.inject ?? [],
149
+ },
150
+ ...defaultProviders(),
151
+ ...toProviders(asyncOptions.overrides),
152
+ ],
153
+ exports: moduleExports(),
154
+ };
155
+ }
156
+ configure(consumer) {
157
+ const resourceServer = this.options.resourceServer;
158
+ if (resourceServer === undefined) {
159
+ return;
160
+ }
161
+ const mcpPath = resourceServer.mcpPath ?? "/mcp";
162
+ consumer.apply(protectedResourceMetadataHandler(resourceServer)).forRoutes({
163
+ path: protectedResourceMetadataPath(mcpPath),
164
+ method: RequestMethod.GET,
165
+ });
166
+ consumer
167
+ .apply(requireBearerAuth({
168
+ verifier: withAudienceCheck(resourceServer.verifier, resourceServer.resource),
169
+ resourceMetadataUrl: protectedResourceMetadataUrl(resourceServer.resource, mcpPath).toString(),
170
+ }))
171
+ .forRoutes({ path: mcpPath, method: RequestMethod.ALL });
172
+ }
173
+ };
174
+ SezzleeModule = SezzleeModule_1 = __decorate([
175
+ Global(),
176
+ Module({}),
177
+ __param(0, Inject(SEZZLEE_OPTIONS)),
178
+ __metadata("design:paramtypes", [SezzleeOptions])
179
+ ], SezzleeModule);
180
+ export { SezzleeModule };
@@ -0,0 +1,18 @@
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+ import { SezzleeDispatchAborted, type DispatchAbortReason } from "@sezzlee/core";
3
+ import type { DispatchResult } from "./dispatcher.js";
4
+ import type { OuterConnection } from "./outer-connection.js";
5
+ export { SezzleeDispatchAborted };
6
+ export type { DispatchAbortReason };
7
+ export interface SyntheticContext {
8
+ req: IncomingMessage;
9
+ res: ServerResponse;
10
+ result: Promise<DispatchResult>;
11
+ /**
12
+ * Abandons the dispatch and delivers a client disconnect to the handler.
13
+ *
14
+ * @param reason why the dispatch was abandoned
15
+ */
16
+ abort(reason: DispatchAbortReason): void;
17
+ }
18
+ export declare function createSyntheticContext(method: string, url: string, headers: Record<string, string>, scheme: string, body?: Buffer, connection?: OuterConnection): SyntheticContext;
@@ -0,0 +1,131 @@
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+ import { Socket } from "node:net";
3
+ import { SezzleeDispatchAborted, } from "@sezzlee/core";
4
+ export { SezzleeDispatchAborted };
5
+ function captureHeaders(res) {
6
+ const headers = {};
7
+ for (const [name, value] of Object.entries(res.getHeaders())) {
8
+ if (value === undefined) {
9
+ continue;
10
+ }
11
+ headers[name.toLowerCase()] = Array.isArray(value)
12
+ ? value.join(", ")
13
+ : String(value);
14
+ }
15
+ return headers;
16
+ }
17
+ function reflectConnection(socket, connection) {
18
+ if (connection === undefined) {
19
+ return;
20
+ }
21
+ for (const [name, value] of Object.entries(connection)) {
22
+ if (value !== undefined) {
23
+ Object.defineProperty(socket, name, { value, configurable: true });
24
+ }
25
+ }
26
+ }
27
+ export function createSyntheticContext(method, url, headers, scheme, body, connection) {
28
+ const socket = new Socket();
29
+ if (scheme === "https") {
30
+ Object.defineProperty(socket, "encrypted", { value: true });
31
+ }
32
+ reflectConnection(socket, connection);
33
+ const req = new IncomingMessage(socket);
34
+ req.method = method;
35
+ req.url = url;
36
+ req.headers = { ...headers };
37
+ if (body !== undefined) {
38
+ req.headers["content-length"] = String(body.byteLength);
39
+ req.push(body);
40
+ }
41
+ req.push(null);
42
+ /**
43
+ * Node's HTTP parser sets `complete`; nothing does for a hand-built message. Left false, the
44
+ * stream's auto-destroy emits `aborted` as soon as the body is consumed, so every dispatch looked
45
+ * like a client disconnect — multer answers it with a 500. Pinned by N2 in
46
+ * test/form-body-probe.spec.ts.
47
+ */
48
+ req.complete = true;
49
+ const res = new ServerResponse(req);
50
+ const chunks = [];
51
+ let settled = false;
52
+ let abandon;
53
+ const result = new Promise((resolve, reject) => {
54
+ const capture = (chunk, encoding) => {
55
+ if (settled) {
56
+ return;
57
+ }
58
+ if (typeof chunk === "string") {
59
+ chunks.push(Buffer.from(chunk, typeof encoding === "string"
60
+ ? encoding
61
+ : "utf8"));
62
+ }
63
+ else if (chunk instanceof Buffer || chunk instanceof Uint8Array) {
64
+ chunks.push(Buffer.from(chunk));
65
+ }
66
+ };
67
+ res.write = function write(chunk, encoding, callback) {
68
+ capture(chunk, encoding);
69
+ const done = typeof encoding === "function" ? encoding : callback;
70
+ if (typeof done === "function") {
71
+ done();
72
+ }
73
+ return true;
74
+ };
75
+ res.end = function end(chunk, encoding, callback) {
76
+ if (settled) {
77
+ return res;
78
+ }
79
+ if (typeof chunk !== "function") {
80
+ capture(chunk, encoding);
81
+ }
82
+ const done = [chunk, encoding, callback].find((arg) => typeof arg === "function");
83
+ const responseHeaders = captureHeaders(res);
84
+ settled = true;
85
+ resolve({
86
+ status: res.statusCode,
87
+ body: Buffer.concat(chunks).toString("utf8"),
88
+ contentType: responseHeaders["content-type"],
89
+ headers: responseHeaders,
90
+ });
91
+ res.emit("finish");
92
+ res.emit("close");
93
+ if (typeof done === "function") {
94
+ done();
95
+ }
96
+ return res;
97
+ };
98
+ /**
99
+ * `settled` is the single guard: without it an abandoned handler that later calls `res.end()`
100
+ * resolves an already-rejected dispatch, emits a second `finish`/`close` into Nest's own
101
+ * listeners, and mutates `chunks` after the body string was built from it. Nest does not
102
+ * unsubscribe a route handler on disconnect, so an abandoned handler running to completion is
103
+ * the normal case, not the exception. Pinned by the abandoned-handler tests in
104
+ * test/dispatch-abort.spec.ts.
105
+ */
106
+ abandon = (reason) => {
107
+ if (settled) {
108
+ return;
109
+ }
110
+ settled = true;
111
+ reject(new SezzleeDispatchAborted(reason));
112
+ try {
113
+ req.emit("aborted");
114
+ req.emit("close");
115
+ res.emit("close");
116
+ socket.emit("close");
117
+ }
118
+ catch (error) {
119
+ process.stderr.write(`sezzlee: a disconnect listener threw: ${String(error)}\n`);
120
+ }
121
+ };
122
+ });
123
+ return {
124
+ req,
125
+ res,
126
+ result,
127
+ abort: (reason) => {
128
+ abandon?.(reason);
129
+ },
130
+ };
131
+ }
@@ -0,0 +1,2 @@
1
+ import type { OAuthTokenVerifier } from "@modelcontextprotocol/express";
2
+ export declare function withAudienceCheck(inner: OAuthTokenVerifier, resource: URL): OAuthTokenVerifier;
@@ -0,0 +1,21 @@
1
+ import { OAuthError, OAuthErrorCode } from "@modelcontextprotocol/server";
2
+ function normalize(url) {
3
+ return url.toString().replace(/\/$/, "");
4
+ }
5
+ export function withAudienceCheck(inner, resource) {
6
+ return {
7
+ async verifyAccessToken(token) {
8
+ const authInfo = await inner.verifyAccessToken(token);
9
+ if (authInfo.resource === undefined ||
10
+ normalize(authInfo.resource) !== normalize(resource)) {
11
+ /**
12
+ * Guard: the express middleware recognises only the v2 `OAuthError`; a legacy error class
13
+ * escapes as a 500 and the client never sees the `invalid_token` challenge that tells it to
14
+ * re-authorize.
15
+ */
16
+ throw new OAuthError(OAuthErrorCode.InvalidToken, "audience mismatch");
17
+ }
18
+ return authInfo;
19
+ },
20
+ };
21
+ }
@@ -0,0 +1,6 @@
1
+ import type { RequestHandler } from "express";
2
+ import type { SezzleeResourceServerOptions } from "../options.js";
3
+ export declare const protectedResourceMetadataWellKnownPrefix = "/.well-known/oauth-protected-resource";
4
+ export declare function protectedResourceMetadataPath(mcpPath: string): string;
5
+ export declare function protectedResourceMetadataUrl(resource: URL, mcpPath: string): URL;
6
+ export declare function protectedResourceMetadataHandler(options: SezzleeResourceServerOptions): RequestHandler;
@@ -0,0 +1,23 @@
1
+ export const protectedResourceMetadataWellKnownPrefix = "/.well-known/oauth-protected-resource";
2
+ export function protectedResourceMetadataPath(mcpPath) {
3
+ return `${protectedResourceMetadataWellKnownPrefix}${mcpPath}`;
4
+ }
5
+ export function protectedResourceMetadataUrl(resource, mcpPath) {
6
+ return new URL(protectedResourceMetadataPath(mcpPath), resource.origin);
7
+ }
8
+ export function protectedResourceMetadataHandler(options) {
9
+ return (_request, response) => {
10
+ response.set("Cache-Control", "public, max-age=300");
11
+ response.json({
12
+ resource: options.resource.toString(),
13
+ authorization_servers: options.authorizationServers.map((url) => url.toString()),
14
+ ...(options.scopesSupported !== undefined
15
+ ? { scopes_supported: options.scopesSupported }
16
+ : {}),
17
+ ...(options.resourceName !== undefined
18
+ ? { resource_name: options.resourceName }
19
+ : {}),
20
+ bearer_methods_supported: ["header"],
21
+ });
22
+ };
23
+ }
@@ -0,0 +1,25 @@
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
+ import type { Request, Response } from "express";
3
+ export type SezzleeServerFactory = () => McpServer;
4
+ export type SezzleeRequestHandler = (req: Request, res: Response) => Promise<void>;
5
+ /**
6
+ * Serves the MCP endpoint over Streamable HTTP on both protocol eras.
7
+ *
8
+ * The 2026-07-28 revision has no sessions and no `Mcp-Session-Id`, so there is nothing to store
9
+ * between requests: `createMcpHandler` builds one server per exchange from the factory, and its
10
+ * `legacy: 'stateless'` default answers 2025-era traffic the same way. A catalogue change reaches
11
+ * clients through the handler's `subscriptions/listen` bus rather than an unsolicited broadcast,
12
+ * which is the only delivery the 2026 revision has.
13
+ */
14
+ export declare class SezzleeStreamableHttp {
15
+ private readonly served;
16
+ /**
17
+ * Binds one endpoint to a server factory and returns its request handler.
18
+ *
19
+ * Guard: call this once per endpoint and keep the result — the handler owns the change-event bus
20
+ * every open `subscriptions/listen` stream is attached to, so building a fresh one per request
21
+ * would leave every subscriber listening to a bus nobody publishes on.
22
+ */
23
+ serve(createServer: SezzleeServerFactory): SezzleeRequestHandler;
24
+ notifyToolListChanged(): void;
25
+ }
@@ -0,0 +1,52 @@
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
+ import { Injectable } from "@nestjs/common";
8
+ import { createMcpHandler } from "@modelcontextprotocol/server";
9
+ import { toNodeHandler } from "@modelcontextprotocol/node";
10
+ import { connectionOf, runWithOuterConnection } from "../outer-connection.js";
11
+ /**
12
+ * Serves the MCP endpoint over Streamable HTTP on both protocol eras.
13
+ *
14
+ * The 2026-07-28 revision has no sessions and no `Mcp-Session-Id`, so there is nothing to store
15
+ * between requests: `createMcpHandler` builds one server per exchange from the factory, and its
16
+ * `legacy: 'stateless'` default answers 2025-era traffic the same way. A catalogue change reaches
17
+ * clients through the handler's `subscriptions/listen` bus rather than an unsolicited broadcast,
18
+ * which is the only delivery the 2026 revision has.
19
+ */
20
+ let SezzleeStreamableHttp = class SezzleeStreamableHttp {
21
+ served = [];
22
+ /**
23
+ * Binds one endpoint to a server factory and returns its request handler.
24
+ *
25
+ * Guard: call this once per endpoint and keep the result — the handler owns the change-event bus
26
+ * every open `subscriptions/listen` stream is attached to, so building a fresh one per request
27
+ * would leave every subscriber listening to a bus nobody publishes on.
28
+ */
29
+ serve(createServer) {
30
+ const handler = createMcpHandler(createServer);
31
+ const dispatchNode = toNodeHandler(handler);
32
+ const dispatch = async (req, res) => runWithOuterConnection(connectionOf(req), async () => {
33
+ await dispatchNode(req, res, req.body);
34
+ });
35
+ this.served.push({
36
+ dispatch,
37
+ notifyToolsChanged: () => {
38
+ handler.notify.toolsChanged();
39
+ },
40
+ });
41
+ return dispatch;
42
+ }
43
+ notifyToolListChanged() {
44
+ for (const endpoint of this.served) {
45
+ endpoint.notifyToolsChanged();
46
+ }
47
+ }
48
+ };
49
+ SezzleeStreamableHttp = __decorate([
50
+ Injectable()
51
+ ], SezzleeStreamableHttp);
52
+ export { SezzleeStreamableHttp };