@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.
- package/LICENSE +21 -0
- package/README.md +360 -2
- package/dist/cache.d.ts +18 -0
- package/dist/cache.js +55 -0
- package/dist/catalog.d.ts +100 -0
- package/dist/catalog.js +456 -0
- package/dist/decorators.d.ts +172 -0
- package/dist/decorators.js +79 -0
- package/dist/discovery/body-encoding.d.ts +24 -0
- package/dist/discovery/body-encoding.js +211 -0
- package/dist/discovery/diagnostics.d.ts +4 -0
- package/dist/discovery/diagnostics.js +49 -0
- package/dist/discovery/endpoint-discovery.d.ts +52 -0
- package/dist/discovery/endpoint-discovery.js +722 -0
- package/dist/discovery/type-shape.d.ts +21 -0
- package/dist/discovery/type-shape.js +297 -0
- package/dist/dispatcher.d.ts +29 -0
- package/dist/dispatcher.js +182 -0
- package/dist/extension-points.d.ts +34 -0
- package/dist/extension-points.js +35 -0
- package/dist/families.d.ts +77 -0
- package/dist/families.js +157 -0
- package/dist/files.d.ts +43 -0
- package/dist/files.js +13 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +26 -0
- package/dist/invoke-result-mapper.d.ts +16 -0
- package/dist/invoke-result-mapper.js +44 -0
- package/dist/markers.d.ts +5 -0
- package/dist/markers.js +27 -0
- package/dist/meta-tools.d.ts +18 -0
- package/dist/meta-tools.js +306 -0
- package/dist/options-validation.d.ts +17 -0
- package/dist/options-validation.js +79 -0
- package/dist/options.d.ts +201 -0
- package/dist/options.js +151 -0
- package/dist/outer-connection.d.ts +18 -0
- package/dist/outer-connection.js +23 -0
- package/dist/sezzlee.module.d.ts +16 -0
- package/dist/sezzlee.module.js +180 -0
- package/dist/synthetic-context.d.ts +18 -0
- package/dist/synthetic-context.js +131 -0
- package/dist/transport/audience.d.ts +2 -0
- package/dist/transport/audience.js +21 -0
- package/dist/transport/protected-resource-metadata.d.ts +6 -0
- package/dist/transport/protected-resource-metadata.js +23 -0
- package/dist/transport/streamable-http.d.ts +25 -0
- package/dist/transport/streamable-http.js +52 -0
- package/dist/visibility/evaluator.d.ts +8 -0
- package/dist/visibility/evaluator.js +5 -0
- package/dist/visibility/probe.d.ts +25 -0
- package/dist/visibility/probe.js +104 -0
- package/dist/visibility/provider.d.ts +23 -0
- package/dist/visibility/provider.js +118 -0
- package/package.json +99 -3
package/dist/options.js
ADDED
|
@@ -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,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 };
|