toolception 0.3.0 → 0.4.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/README.md +50 -28
- package/dist/core/DynamicToolManager.d.ts +45 -2
- package/dist/core/DynamicToolManager.d.ts.map +1 -1
- package/dist/core/ServerOrchestrator.d.ts +24 -2
- package/dist/core/ServerOrchestrator.d.ts.map +1 -1
- package/dist/http/FastifyTransport.d.ts +11 -0
- package/dist/http/FastifyTransport.d.ts.map +1 -1
- package/dist/index.js +679 -451
- package/dist/index.js.map +1 -1
- package/dist/meta/registerMetaTools.d.ts +14 -0
- package/dist/meta/registerMetaTools.d.ts.map +1 -1
- package/dist/mode/ModeResolver.d.ts +7 -0
- package/dist/mode/ModeResolver.d.ts.map +1 -1
- package/dist/permissions/PermissionAwareFastifyTransport.d.ts +2 -1
- package/dist/permissions/PermissionAwareFastifyTransport.d.ts.map +1 -1
- package/dist/permissions/PermissionResolver.d.ts +12 -0
- package/dist/permissions/PermissionResolver.d.ts.map +1 -1
- package/dist/permissions/createPermissionAwareBundle.d.ts +6 -0
- package/dist/permissions/createPermissionAwareBundle.d.ts.map +1 -1
- package/dist/server/createMcpServer.d.ts +4 -3
- package/dist/server/createMcpServer.d.ts.map +1 -1
- package/dist/server/createPermissionBasedMcpServer.d.ts.map +1 -1
- package/dist/session/ClientResourceCache.d.ts +34 -3
- package/dist/session/ClientResourceCache.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
var
|
|
1
|
+
var A = (r) => {
|
|
2
2
|
throw TypeError(r);
|
|
3
3
|
};
|
|
4
|
-
var
|
|
5
|
-
var
|
|
6
|
-
var u = (r, e,
|
|
7
|
-
import { z as
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import { randomUUID as
|
|
11
|
-
import { StreamableHTTPServerTransport as
|
|
12
|
-
import { isInitializeRequest as
|
|
13
|
-
const
|
|
4
|
+
var W = (r, e, s) => e.has(r) || A("Cannot " + s);
|
|
5
|
+
var y = (r, e, s) => e.has(r) ? A("Cannot add the same private member more than once") : e instanceof WeakSet ? e.add(r) : e.set(r, s);
|
|
6
|
+
var u = (r, e, s) => (W(r, e, "access private method"), s);
|
|
7
|
+
import { z as w } from "zod";
|
|
8
|
+
import M from "fastify";
|
|
9
|
+
import x from "@fastify/cors";
|
|
10
|
+
import { randomUUID as v } from "node:crypto";
|
|
11
|
+
import { StreamableHTTPServerTransport as E } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
12
|
+
import { isInitializeRequest as I } from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
const S = {
|
|
14
14
|
dynamic: [
|
|
15
15
|
"dynamic-tool-discovery",
|
|
16
16
|
"dynamicToolDiscovery",
|
|
@@ -18,89 +18,98 @@ const b = {
|
|
|
18
18
|
],
|
|
19
19
|
toolsets: ["tool-sets", "toolSets", "FMP_TOOL_SETS"]
|
|
20
20
|
};
|
|
21
|
-
class
|
|
21
|
+
class q {
|
|
22
22
|
constructor(e = {}) {
|
|
23
23
|
this.keys = {
|
|
24
|
-
dynamic: e.keys?.dynamic ??
|
|
25
|
-
toolsets: e.keys?.toolsets ??
|
|
24
|
+
dynamic: e.keys?.dynamic ?? S.dynamic,
|
|
25
|
+
toolsets: e.keys?.toolsets ?? S.toolsets
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
resolveMode(e,
|
|
29
|
-
return this.isDynamicEnabled(
|
|
28
|
+
resolveMode(e, s) {
|
|
29
|
+
return this.isDynamicEnabled(s) ? "DYNAMIC" : this.getToolsetsString(s) ? "STATIC" : this.isDynamicEnabled(e) ? "DYNAMIC" : this.getToolsetsString(e) ? "STATIC" : null;
|
|
30
30
|
}
|
|
31
|
-
parseCommaSeparatedToolSets(e,
|
|
31
|
+
parseCommaSeparatedToolSets(e, s) {
|
|
32
32
|
if (!e || typeof e != "string") return [];
|
|
33
|
-
const
|
|
34
|
-
for (const
|
|
35
|
-
o.has(
|
|
36
|
-
`Invalid toolset '${
|
|
33
|
+
const t = e.split(",").map((n) => n.trim()).filter((n) => n.length > 0), o = new Set(Object.keys(s)), i = [];
|
|
34
|
+
for (const n of t)
|
|
35
|
+
o.has(n) ? i.push(n) : console.warn(
|
|
36
|
+
`Invalid toolset '${n}' ignored. Available: ${Array.from(
|
|
37
37
|
o
|
|
38
38
|
).join(", ")}`
|
|
39
39
|
);
|
|
40
40
|
return i;
|
|
41
41
|
}
|
|
42
|
-
getModulesForToolSets(e,
|
|
43
|
-
const
|
|
42
|
+
getModulesForToolSets(e, s) {
|
|
43
|
+
const t = /* @__PURE__ */ new Set();
|
|
44
44
|
for (const o of e) {
|
|
45
|
-
const i =
|
|
46
|
-
i && (i.modules || []).forEach((
|
|
45
|
+
const i = s[o];
|
|
46
|
+
i && (i.modules || []).forEach((n) => t.add(n));
|
|
47
47
|
}
|
|
48
|
-
return Array.from(
|
|
48
|
+
return Array.from(t);
|
|
49
49
|
}
|
|
50
|
-
validateToolsetName(e,
|
|
50
|
+
validateToolsetName(e, s) {
|
|
51
51
|
if (!e || typeof e != "string")
|
|
52
52
|
return {
|
|
53
53
|
isValid: !1,
|
|
54
54
|
error: `Invalid toolset name provided. Must be a non-empty string. Available toolsets: ${Object.keys(
|
|
55
|
-
|
|
55
|
+
s
|
|
56
56
|
).join(", ")}`
|
|
57
57
|
};
|
|
58
|
-
const
|
|
59
|
-
return
|
|
58
|
+
const t = e.trim();
|
|
59
|
+
return t.length === 0 ? {
|
|
60
60
|
isValid: !1,
|
|
61
61
|
error: `Empty toolset name provided. Available toolsets: ${Object.keys(
|
|
62
|
-
|
|
62
|
+
s
|
|
63
63
|
).join(", ")}`
|
|
64
|
-
} : t
|
|
64
|
+
} : s[t] ? { isValid: !0, sanitized: t } : {
|
|
65
65
|
isValid: !1,
|
|
66
|
-
error: `Toolset '${
|
|
67
|
-
|
|
66
|
+
error: `Toolset '${t}' not found. Available toolsets: ${Object.keys(
|
|
67
|
+
s
|
|
68
68
|
).join(", ")}`
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Validates and retrieves modules for a set of toolsets.
|
|
73
|
+
* Note: A toolset with only direct tools (no modules) is valid and returns an empty modules array.
|
|
74
|
+
* @param toolsetNames - Array of toolset names to validate
|
|
75
|
+
* @param catalog - The toolset catalog to validate against
|
|
76
|
+
* @returns Validation result with modules array if valid
|
|
77
|
+
*/
|
|
78
|
+
validateToolsetModules(e, s) {
|
|
72
79
|
try {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
for (const o of e)
|
|
81
|
+
if (!s[o])
|
|
82
|
+
return {
|
|
83
|
+
isValid: !1,
|
|
84
|
+
error: `Toolset '${o}' not found in catalog`
|
|
85
|
+
};
|
|
86
|
+
return { isValid: !0, modules: this.getModulesForToolSets(e, s) };
|
|
87
|
+
} catch (t) {
|
|
79
88
|
return {
|
|
80
89
|
isValid: !1,
|
|
81
|
-
error: `Error resolving modules for ${e.join(", ")}: ${
|
|
90
|
+
error: `Error resolving modules for ${e.join(", ")}: ${t instanceof Error ? t.message : "Unknown error"}`
|
|
82
91
|
};
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
94
|
isDynamicEnabled(e) {
|
|
86
95
|
if (!e) return !1;
|
|
87
|
-
for (const
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
96
|
+
for (const s of this.keys.dynamic) {
|
|
97
|
+
const t = e[s];
|
|
98
|
+
if (t === !0 || typeof t == "string" && t.trim().toLowerCase() === "true")
|
|
90
99
|
return !0;
|
|
91
100
|
}
|
|
92
101
|
return !1;
|
|
93
102
|
}
|
|
94
103
|
getToolsetsString(e) {
|
|
95
104
|
if (e)
|
|
96
|
-
for (const
|
|
97
|
-
const
|
|
98
|
-
if (typeof
|
|
99
|
-
return
|
|
105
|
+
for (const s of this.keys.toolsets) {
|
|
106
|
+
const t = e[s];
|
|
107
|
+
if (typeof t == "string" && t.trim().length > 0)
|
|
108
|
+
return t;
|
|
100
109
|
}
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
|
-
class
|
|
112
|
+
class J {
|
|
104
113
|
constructor(e) {
|
|
105
114
|
this.catalog = e.catalog, this.moduleLoaders = e.moduleLoaders ?? {};
|
|
106
115
|
}
|
|
@@ -118,80 +127,80 @@ class U {
|
|
|
118
127
|
", "
|
|
119
128
|
)}`
|
|
120
129
|
};
|
|
121
|
-
const
|
|
122
|
-
return
|
|
130
|
+
const s = e.trim();
|
|
131
|
+
return s.length === 0 ? {
|
|
123
132
|
isValid: !1,
|
|
124
133
|
error: `Empty toolset name provided. Available toolsets: ${this.getAvailableToolsets().join(
|
|
125
134
|
", "
|
|
126
135
|
)}`
|
|
127
|
-
} : this.catalog[
|
|
136
|
+
} : this.catalog[s] ? { isValid: !0, sanitized: s } : {
|
|
128
137
|
isValid: !1,
|
|
129
|
-
error: `Toolset '${
|
|
138
|
+
error: `Toolset '${s}' not found. Available toolsets: ${this.getAvailableToolsets().join(
|
|
130
139
|
", "
|
|
131
140
|
)}`
|
|
132
141
|
};
|
|
133
142
|
}
|
|
134
|
-
async resolveToolsForToolsets(e,
|
|
135
|
-
const
|
|
143
|
+
async resolveToolsForToolsets(e, s) {
|
|
144
|
+
const t = [];
|
|
136
145
|
for (const o of e) {
|
|
137
146
|
const i = this.catalog[o];
|
|
138
|
-
if (i && (Array.isArray(i.tools) && i.tools.length > 0 &&
|
|
139
|
-
for (const
|
|
140
|
-
const l = this.moduleLoaders[
|
|
147
|
+
if (i && (Array.isArray(i.tools) && i.tools.length > 0 && t.push(...i.tools), Array.isArray(i.modules) && i.modules.length > 0))
|
|
148
|
+
for (const n of i.modules) {
|
|
149
|
+
const l = this.moduleLoaders[n];
|
|
141
150
|
if (l)
|
|
142
151
|
try {
|
|
143
|
-
const
|
|
144
|
-
Array.isArray(
|
|
145
|
-
} catch (
|
|
152
|
+
const a = await l(s);
|
|
153
|
+
Array.isArray(a) && a.length > 0 && t.push(...a);
|
|
154
|
+
} catch (a) {
|
|
146
155
|
console.warn(
|
|
147
|
-
`Module loader '${
|
|
148
|
-
|
|
156
|
+
`Module loader '${n}' failed for toolset '${o}':`,
|
|
157
|
+
a
|
|
149
158
|
);
|
|
150
159
|
}
|
|
151
160
|
}
|
|
152
161
|
}
|
|
153
|
-
return
|
|
162
|
+
return t;
|
|
154
163
|
}
|
|
155
164
|
}
|
|
156
|
-
class
|
|
157
|
-
constructor(e,
|
|
158
|
-
super(e), this.name = "ToolingError", this.code =
|
|
165
|
+
class C extends Error {
|
|
166
|
+
constructor(e, s, t, o) {
|
|
167
|
+
super(e), this.name = "ToolingError", this.code = s, this.details = t;
|
|
159
168
|
}
|
|
160
169
|
}
|
|
161
|
-
class
|
|
170
|
+
class P {
|
|
162
171
|
constructor(e = {}) {
|
|
163
172
|
this.names = /* @__PURE__ */ new Set(), this.toolsetToNames = /* @__PURE__ */ new Map(), this.options = {
|
|
164
173
|
namespaceWithToolset: e.namespaceWithToolset ?? !0
|
|
165
174
|
};
|
|
166
175
|
}
|
|
167
|
-
getSafeName(e,
|
|
168
|
-
return !this.options.namespaceWithToolset ||
|
|
176
|
+
getSafeName(e, s) {
|
|
177
|
+
return !this.options.namespaceWithToolset || s.startsWith(`${e}.`) ? s : `${e}.${s}`;
|
|
169
178
|
}
|
|
170
179
|
has(e) {
|
|
171
180
|
return this.names.has(e);
|
|
172
181
|
}
|
|
173
182
|
add(e) {
|
|
174
183
|
if (this.names.has(e))
|
|
175
|
-
throw new
|
|
184
|
+
throw new C(
|
|
176
185
|
`Tool name collision: '${e}' already registered`,
|
|
177
186
|
"E_TOOL_NAME_CONFLICT"
|
|
178
187
|
);
|
|
179
188
|
this.names.add(e);
|
|
180
189
|
}
|
|
181
|
-
addForToolset(e,
|
|
182
|
-
this.add(
|
|
183
|
-
const
|
|
184
|
-
|
|
190
|
+
addForToolset(e, s) {
|
|
191
|
+
this.add(s);
|
|
192
|
+
const t = this.toolsetToNames.get(e) ?? /* @__PURE__ */ new Set();
|
|
193
|
+
t.add(s), this.toolsetToNames.set(e, t);
|
|
185
194
|
}
|
|
186
|
-
mapAndValidate(e,
|
|
187
|
-
return
|
|
188
|
-
const o = this.getSafeName(e,
|
|
195
|
+
mapAndValidate(e, s) {
|
|
196
|
+
return s.map((t) => {
|
|
197
|
+
const o = this.getSafeName(e, t.name);
|
|
189
198
|
if (this.has(o))
|
|
190
|
-
throw new
|
|
199
|
+
throw new C(
|
|
191
200
|
`Tool name collision for '${o}'`,
|
|
192
201
|
"E_TOOL_NAME_CONFLICT"
|
|
193
202
|
);
|
|
194
|
-
return { ...
|
|
203
|
+
return { ...t, name: o };
|
|
195
204
|
});
|
|
196
205
|
}
|
|
197
206
|
list() {
|
|
@@ -199,14 +208,28 @@ class I {
|
|
|
199
208
|
}
|
|
200
209
|
listByToolset() {
|
|
201
210
|
const e = {};
|
|
202
|
-
for (const [
|
|
203
|
-
e[
|
|
211
|
+
for (const [s, t] of this.toolsetToNames.entries())
|
|
212
|
+
e[s] = Array.from(t);
|
|
204
213
|
return e;
|
|
205
214
|
}
|
|
206
215
|
}
|
|
207
|
-
class
|
|
216
|
+
class G {
|
|
208
217
|
constructor(e) {
|
|
209
|
-
this.activeToolsets = /* @__PURE__ */ new Set(), this.server = e.server, this.resolver = e.resolver, this.context = e.context, this.onToolsListChanged = e.onToolsListChanged, this.exposurePolicy = e.exposurePolicy, this.toolRegistry = e.toolRegistry ?? new
|
|
218
|
+
this.activeToolsets = /* @__PURE__ */ new Set(), this.server = e.server, this.resolver = e.resolver, this.context = e.context, this.onToolsListChanged = e.onToolsListChanged, this.exposurePolicy = e.exposurePolicy, this.toolRegistry = e.toolRegistry ?? new P({ namespaceWithToolset: !0 });
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Sends a tool list change notification if configured.
|
|
222
|
+
* Logs warnings on failure instead of throwing.
|
|
223
|
+
* @returns Promise that resolves when notification is sent (or skipped)
|
|
224
|
+
* @private
|
|
225
|
+
*/
|
|
226
|
+
async notifyToolsChanged() {
|
|
227
|
+
if (this.onToolsListChanged)
|
|
228
|
+
try {
|
|
229
|
+
await this.onToolsListChanged();
|
|
230
|
+
} catch (e) {
|
|
231
|
+
console.warn("Failed to send tool list change notification:", e);
|
|
232
|
+
}
|
|
210
233
|
}
|
|
211
234
|
getAvailableToolsets() {
|
|
212
235
|
return this.resolver.getAvailableToolsets();
|
|
@@ -220,90 +243,113 @@ class W {
|
|
|
220
243
|
isActive(e) {
|
|
221
244
|
return this.activeToolsets.has(e);
|
|
222
245
|
}
|
|
223
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Enables a single toolset by name.
|
|
248
|
+
* Validates the toolset, checks exposure policies, resolves tools, and registers them.
|
|
249
|
+
* @param toolsetName - The name of the toolset to enable
|
|
250
|
+
* @param skipNotification - If true, skips the tool list change notification (for batch operations)
|
|
251
|
+
* @returns Result object with success status and message
|
|
252
|
+
*/
|
|
253
|
+
async enableToolset(e, s = !1) {
|
|
224
254
|
const t = this.resolver.validateToolsetName(e);
|
|
225
255
|
if (!t.isValid || !t.sanitized)
|
|
226
256
|
return {
|
|
227
257
|
success: !1,
|
|
228
258
|
message: t.error || "Unknown validation error"
|
|
229
259
|
};
|
|
230
|
-
const
|
|
231
|
-
if (this.activeToolsets.has(
|
|
260
|
+
const o = t.sanitized;
|
|
261
|
+
if (this.activeToolsets.has(o))
|
|
232
262
|
return {
|
|
233
263
|
success: !1,
|
|
234
|
-
message: `Toolset '${
|
|
264
|
+
message: `Toolset '${o}' is already enabled.`
|
|
235
265
|
};
|
|
266
|
+
const i = this.checkExposurePolicy(o);
|
|
267
|
+
if (!i.allowed)
|
|
268
|
+
return { success: !1, message: i.message };
|
|
269
|
+
const n = [];
|
|
236
270
|
try {
|
|
237
|
-
const
|
|
238
|
-
[
|
|
271
|
+
const l = await this.resolver.resolveToolsForToolsets(
|
|
272
|
+
[o],
|
|
239
273
|
this.context
|
|
240
274
|
);
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
};
|
|
246
|
-
if (this.exposurePolicy?.denylist && this.exposurePolicy.denylist.includes(s))
|
|
247
|
-
return {
|
|
248
|
-
success: !1,
|
|
249
|
-
message: `Toolset '${s}' is denied by policy.`
|
|
250
|
-
};
|
|
251
|
-
if (this.exposurePolicy?.maxActiveToolsets !== void 0 && this.activeToolsets.size + 1 > this.exposurePolicy.maxActiveToolsets)
|
|
252
|
-
return this.exposurePolicy.onLimitExceeded?.(
|
|
253
|
-
[s],
|
|
254
|
-
Array.from(this.activeToolsets)
|
|
255
|
-
), {
|
|
256
|
-
success: !1,
|
|
257
|
-
message: `Activation exceeds maxActiveToolsets (${this.exposurePolicy.maxActiveToolsets}).`
|
|
258
|
-
};
|
|
259
|
-
if (o && o.length > 0) {
|
|
260
|
-
const i = this.toolRegistry.mapAndValidate(
|
|
261
|
-
s,
|
|
262
|
-
o
|
|
275
|
+
if (l && l.length > 0) {
|
|
276
|
+
const a = this.toolRegistry.mapAndValidate(
|
|
277
|
+
o,
|
|
278
|
+
l
|
|
263
279
|
);
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
this.activeToolsets.add(s);
|
|
267
|
-
try {
|
|
268
|
-
await this.onToolsListChanged?.();
|
|
269
|
-
} catch (i) {
|
|
270
|
-
console.warn("Failed to send tool list change notification:", i);
|
|
280
|
+
for (const c of a)
|
|
281
|
+
this.registerSingleTool(c, o), n.push(c.name);
|
|
271
282
|
}
|
|
272
|
-
return {
|
|
283
|
+
return this.activeToolsets.add(o), s || await this.notifyToolsChanged(), {
|
|
273
284
|
success: !0,
|
|
274
|
-
message: `Toolset '${
|
|
285
|
+
message: `Toolset '${o}' enabled successfully. Registered ${l?.length ?? 0} tools.`
|
|
275
286
|
};
|
|
276
|
-
} catch (
|
|
277
|
-
return
|
|
287
|
+
} catch (l) {
|
|
288
|
+
return n.length > 0 && console.warn(
|
|
289
|
+
`Partial failure enabling toolset '${o}'. ${n.length} tools were registered but toolset activation failed. Tools remain registered due to MCP limitations: ${n.join(", ")}`
|
|
290
|
+
), {
|
|
278
291
|
success: !1,
|
|
279
|
-
message: `Failed to enable toolset '${
|
|
292
|
+
message: `Failed to enable toolset '${o}': ${l instanceof Error ? l.message : "Unknown error"}`
|
|
280
293
|
};
|
|
281
294
|
}
|
|
282
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Checks if a toolset is allowed by the exposure policy.
|
|
298
|
+
* @param toolsetName - The sanitized toolset name to check
|
|
299
|
+
* @returns Object indicating if allowed and reason message if not
|
|
300
|
+
* @private
|
|
301
|
+
*/
|
|
302
|
+
checkExposurePolicy(e) {
|
|
303
|
+
return this.exposurePolicy?.allowlist && !this.exposurePolicy.allowlist.includes(e) ? {
|
|
304
|
+
allowed: !1,
|
|
305
|
+
message: `Toolset '${e}' is not allowed by policy.`
|
|
306
|
+
} : this.exposurePolicy?.denylist && this.exposurePolicy.denylist.includes(e) ? {
|
|
307
|
+
allowed: !1,
|
|
308
|
+
message: `Toolset '${e}' is denied by policy.`
|
|
309
|
+
} : this.exposurePolicy?.maxActiveToolsets !== void 0 && this.activeToolsets.size + 1 > this.exposurePolicy.maxActiveToolsets ? (this.exposurePolicy.onLimitExceeded?.(
|
|
310
|
+
[e],
|
|
311
|
+
Array.from(this.activeToolsets)
|
|
312
|
+
), {
|
|
313
|
+
allowed: !1,
|
|
314
|
+
message: `Activation exceeds maxActiveToolsets (${this.exposurePolicy.maxActiveToolsets}).`
|
|
315
|
+
}) : { allowed: !0, message: "" };
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Registers a single tool with the MCP server.
|
|
319
|
+
* @param tool - The tool definition to register
|
|
320
|
+
* @param toolsetKey - The toolset key for tracking
|
|
321
|
+
* @private
|
|
322
|
+
*/
|
|
323
|
+
registerSingleTool(e, s) {
|
|
324
|
+
this.server.tool(
|
|
325
|
+
e.name,
|
|
326
|
+
e.description,
|
|
327
|
+
e.inputSchema,
|
|
328
|
+
async (t) => await e.handler(t)
|
|
329
|
+
), this.toolRegistry.addForToolset(s, e.name);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Disables a toolset by name.
|
|
333
|
+
* Note: Due to MCP limitations, tools remain registered but the toolset is marked inactive.
|
|
334
|
+
* @param toolsetName - The name of the toolset to disable
|
|
335
|
+
* @returns Result object with success status and message
|
|
336
|
+
*/
|
|
283
337
|
async disableToolset(e) {
|
|
284
|
-
const
|
|
285
|
-
if (!
|
|
338
|
+
const s = this.resolver.validateToolsetName(e);
|
|
339
|
+
if (!s.isValid || !s.sanitized) {
|
|
286
340
|
const o = Array.from(this.activeToolsets).join(", ") || "none";
|
|
287
341
|
return {
|
|
288
342
|
success: !1,
|
|
289
|
-
message: `${
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
const s = t.sanitized;
|
|
293
|
-
if (!this.activeToolsets.has(s))
|
|
294
|
-
return {
|
|
295
|
-
success: !1,
|
|
296
|
-
message: `Toolset '${s}' is not currently active. Active toolsets: ${Array.from(this.activeToolsets).join(", ") || "none"}`
|
|
343
|
+
message: `${s.error || "Unknown validation error"} Active toolsets: ${o}`
|
|
297
344
|
};
|
|
298
|
-
this.activeToolsets.delete(s);
|
|
299
|
-
try {
|
|
300
|
-
await this.onToolsListChanged?.();
|
|
301
|
-
} catch (o) {
|
|
302
|
-
console.warn("Failed to send tool list change notification:", o);
|
|
303
345
|
}
|
|
304
|
-
|
|
346
|
+
const t = s.sanitized;
|
|
347
|
+
return this.activeToolsets.has(t) ? (this.activeToolsets.delete(t), await this.notifyToolsChanged(), {
|
|
305
348
|
success: !0,
|
|
306
|
-
message: `Toolset '${
|
|
349
|
+
message: `Toolset '${t}' disabled successfully. Individual tools remain registered due to MCP limitations.`
|
|
350
|
+
}) : {
|
|
351
|
+
success: !1,
|
|
352
|
+
message: `Toolset '${t}' is not currently active. Active toolsets: ${Array.from(this.activeToolsets).join(", ") || "none"}`
|
|
307
353
|
};
|
|
308
354
|
}
|
|
309
355
|
getStatus() {
|
|
@@ -317,121 +363,112 @@ class W {
|
|
|
317
363
|
toolsetToTools: this.toolRegistry.listByToolset()
|
|
318
364
|
};
|
|
319
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Enables multiple toolsets in a batch operation.
|
|
368
|
+
* Sends a single notification after all toolsets are processed.
|
|
369
|
+
* @param toolsetNames - Array of toolset names to enable
|
|
370
|
+
* @returns Result object with overall success status and individual results
|
|
371
|
+
*/
|
|
320
372
|
async enableToolsets(e) {
|
|
321
|
-
const
|
|
322
|
-
for (const
|
|
373
|
+
const s = [];
|
|
374
|
+
for (const n of e)
|
|
323
375
|
try {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
} catch (
|
|
327
|
-
|
|
328
|
-
name:
|
|
376
|
+
const l = await this.enableToolset(n, !0);
|
|
377
|
+
s.push({ name: n, ...l });
|
|
378
|
+
} catch (l) {
|
|
379
|
+
s.push({
|
|
380
|
+
name: n,
|
|
329
381
|
success: !1,
|
|
330
|
-
message:
|
|
382
|
+
message: l instanceof Error ? l.message : "Unknown error",
|
|
331
383
|
code: "E_INTERNAL"
|
|
332
384
|
});
|
|
333
385
|
}
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
try {
|
|
337
|
-
await this.onToolsListChanged?.();
|
|
338
|
-
} catch {
|
|
339
|
-
}
|
|
340
|
-
return { success: s, results: t, message: o };
|
|
341
|
-
}
|
|
342
|
-
registerDirectTools(e, t) {
|
|
343
|
-
for (const s of e)
|
|
344
|
-
try {
|
|
345
|
-
this.server.tool(
|
|
346
|
-
s.name,
|
|
347
|
-
s.description,
|
|
348
|
-
s.inputSchema,
|
|
349
|
-
async (o) => await s.handler(o)
|
|
350
|
-
), t ? this.toolRegistry.addForToolset(t, s.name) : this.toolRegistry.add(s.name);
|
|
351
|
-
} catch (o) {
|
|
352
|
-
throw console.error(`Failed to register direct tool '${s.name}':`, o), o;
|
|
353
|
-
}
|
|
386
|
+
const t = s.every((n) => n.success), o = s.some((n) => n.success), i = t ? "All toolsets enabled" : o ? "Some toolsets failed to enable" : "All toolsets failed to enable";
|
|
387
|
+
return o && await this.notifyToolsChanged(), { success: t, results: s, message: i };
|
|
354
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* Enables all available toolsets in a batch operation.
|
|
391
|
+
* @returns Result object with overall success status and individual results
|
|
392
|
+
*/
|
|
355
393
|
async enableAllToolsets() {
|
|
356
394
|
const e = this.getAvailableToolsets();
|
|
357
395
|
return this.enableToolsets(e);
|
|
358
396
|
}
|
|
359
397
|
}
|
|
360
|
-
function
|
|
361
|
-
|
|
362
|
-
r.tool(
|
|
398
|
+
function K(r, e, s) {
|
|
399
|
+
(s?.mode ?? "DYNAMIC") === "DYNAMIC" && (r.tool(
|
|
363
400
|
"enable_toolset",
|
|
364
401
|
"Enable a toolset by name",
|
|
365
|
-
{ name:
|
|
402
|
+
{ name: w.string().describe("Toolset name") },
|
|
366
403
|
async (o) => {
|
|
367
|
-
const
|
|
404
|
+
const i = await e.enableToolset(o.name);
|
|
368
405
|
return {
|
|
369
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
406
|
+
content: [{ type: "text", text: JSON.stringify(i) }]
|
|
370
407
|
};
|
|
371
408
|
}
|
|
372
409
|
), r.tool(
|
|
373
410
|
"disable_toolset",
|
|
374
411
|
"Disable a toolset by name (state only)",
|
|
375
|
-
{ name:
|
|
412
|
+
{ name: w.string().describe("Toolset name") },
|
|
376
413
|
async (o) => {
|
|
377
|
-
const
|
|
414
|
+
const i = await e.disableToolset(o.name);
|
|
378
415
|
return {
|
|
379
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
416
|
+
content: [{ type: "text", text: JSON.stringify(i) }]
|
|
380
417
|
};
|
|
381
418
|
}
|
|
382
|
-
),
|
|
419
|
+
), r.tool(
|
|
383
420
|
"list_toolsets",
|
|
384
421
|
"List available toolsets with active status and definitions",
|
|
385
422
|
{},
|
|
386
423
|
async () => {
|
|
387
|
-
const o = e.getAvailableToolsets(), i = e.getStatus().toolsetToTools,
|
|
388
|
-
const
|
|
424
|
+
const o = e.getAvailableToolsets(), i = e.getStatus().toolsetToTools, n = o.map((l) => {
|
|
425
|
+
const a = e.getToolsetDefinition(l);
|
|
389
426
|
return {
|
|
390
427
|
key: l,
|
|
391
428
|
active: e.isActive(l),
|
|
392
|
-
definition:
|
|
393
|
-
name:
|
|
394
|
-
description:
|
|
395
|
-
modules:
|
|
396
|
-
decisionCriteria:
|
|
429
|
+
definition: a ? {
|
|
430
|
+
name: a.name,
|
|
431
|
+
description: a.description,
|
|
432
|
+
modules: a.modules ?? [],
|
|
433
|
+
decisionCriteria: a.decisionCriteria ?? void 0
|
|
397
434
|
} : null,
|
|
398
435
|
tools: i[l] ?? []
|
|
399
436
|
};
|
|
400
437
|
});
|
|
401
438
|
return {
|
|
402
439
|
content: [
|
|
403
|
-
{ type: "text", text: JSON.stringify({ toolsets:
|
|
440
|
+
{ type: "text", text: JSON.stringify({ toolsets: n }) }
|
|
404
441
|
]
|
|
405
442
|
};
|
|
406
443
|
}
|
|
407
444
|
), r.tool(
|
|
408
445
|
"describe_toolset",
|
|
409
446
|
"Describe a toolset with definition, active status and tools",
|
|
410
|
-
{ name:
|
|
447
|
+
{ name: w.string().describe("Toolset name") },
|
|
411
448
|
async (o) => {
|
|
412
|
-
const
|
|
413
|
-
if (!
|
|
449
|
+
const i = e.getToolsetDefinition(o.name), n = e.getStatus().toolsetToTools;
|
|
450
|
+
if (!i)
|
|
414
451
|
return {
|
|
415
452
|
content: [
|
|
416
453
|
{
|
|
417
454
|
type: "text",
|
|
418
|
-
text: JSON.stringify({ error: `Unknown toolset '${
|
|
455
|
+
text: JSON.stringify({ error: `Unknown toolset '${o.name}'` })
|
|
419
456
|
}
|
|
420
457
|
]
|
|
421
458
|
};
|
|
422
|
-
const
|
|
423
|
-
key:
|
|
424
|
-
active: e.isActive(
|
|
459
|
+
const l = {
|
|
460
|
+
key: o.name,
|
|
461
|
+
active: e.isActive(o.name),
|
|
425
462
|
definition: {
|
|
426
|
-
name:
|
|
427
|
-
description:
|
|
428
|
-
modules:
|
|
429
|
-
decisionCriteria:
|
|
463
|
+
name: i.name,
|
|
464
|
+
description: i.description,
|
|
465
|
+
modules: i.modules ?? [],
|
|
466
|
+
decisionCriteria: i.decisionCriteria ?? void 0
|
|
430
467
|
},
|
|
431
|
-
tools:
|
|
468
|
+
tools: n[o.name] ?? []
|
|
432
469
|
};
|
|
433
470
|
return {
|
|
434
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
471
|
+
content: [{ type: "text", text: JSON.stringify(l) }]
|
|
435
472
|
};
|
|
436
473
|
}
|
|
437
474
|
)), r.tool(
|
|
@@ -449,41 +486,72 @@ function H(r, e, t) {
|
|
|
449
486
|
}
|
|
450
487
|
);
|
|
451
488
|
}
|
|
452
|
-
class
|
|
489
|
+
class T {
|
|
453
490
|
constructor(e) {
|
|
454
|
-
this.toolsetValidator = new
|
|
455
|
-
const
|
|
456
|
-
this.mode =
|
|
491
|
+
this.initError = null, this.toolsetValidator = new q();
|
|
492
|
+
const s = e.startup ?? {}, t = this.resolveStartupConfig(s, e.catalog);
|
|
493
|
+
this.mode = t.mode, this.resolver = new J({
|
|
457
494
|
catalog: e.catalog,
|
|
458
495
|
moduleLoaders: e.moduleLoaders
|
|
459
496
|
});
|
|
460
|
-
const o = new
|
|
497
|
+
const o = new P({
|
|
461
498
|
namespaceWithToolset: e.exposurePolicy?.namespaceToolsWithSetKey ?? !0
|
|
462
499
|
});
|
|
463
|
-
this.manager = new
|
|
500
|
+
this.manager = new G({
|
|
464
501
|
server: e.server,
|
|
465
502
|
resolver: this.resolver,
|
|
466
503
|
context: e.context,
|
|
467
504
|
onToolsListChanged: e.notifyToolsListChanged,
|
|
468
505
|
exposurePolicy: e.exposurePolicy,
|
|
469
506
|
toolRegistry: o
|
|
470
|
-
}), e.registerMetaTools !== !1 &&
|
|
471
|
-
const i =
|
|
472
|
-
|
|
507
|
+
}), e.registerMetaTools !== !1 && K(e.server, this.manager, { mode: this.mode });
|
|
508
|
+
const i = t.toolsets;
|
|
509
|
+
this.initPromise = this.initializeToolsets(i);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Initializes toolsets asynchronously during construction.
|
|
513
|
+
* Stores any errors for later retrieval via ensureReady().
|
|
514
|
+
* @param initial - The toolsets to initialize or "ALL"
|
|
515
|
+
* @returns Promise that resolves when initialization is complete
|
|
516
|
+
* @private
|
|
517
|
+
*/
|
|
518
|
+
async initializeToolsets(e) {
|
|
519
|
+
try {
|
|
520
|
+
e === "ALL" ? await this.manager.enableToolsets(this.resolver.getAvailableToolsets()) : Array.isArray(e) && e.length > 0 && await this.manager.enableToolsets(e);
|
|
521
|
+
} catch (s) {
|
|
522
|
+
this.initError = s instanceof Error ? s : new Error(String(s)), console.error("Failed to initialize toolsets:", this.initError);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Waits for the orchestrator to be fully initialized.
|
|
527
|
+
* Call this before using the orchestrator to ensure all toolsets are loaded.
|
|
528
|
+
* @throws {Error} If initialization failed
|
|
529
|
+
*/
|
|
530
|
+
async ensureReady() {
|
|
531
|
+
if (await this.initPromise, this.initError)
|
|
532
|
+
throw this.initError;
|
|
473
533
|
}
|
|
474
|
-
|
|
534
|
+
/**
|
|
535
|
+
* Checks if the orchestrator has finished initialization.
|
|
536
|
+
* Does not throw on error - use ensureReady() for that.
|
|
537
|
+
* @returns Promise that resolves to true if ready, false if initialization failed
|
|
538
|
+
*/
|
|
539
|
+
async isReady() {
|
|
540
|
+
return await this.initPromise, this.initError === null;
|
|
541
|
+
}
|
|
542
|
+
resolveStartupConfig(e, s) {
|
|
475
543
|
if (e.mode) {
|
|
476
544
|
if (e.mode === "DYNAMIC" && e.toolsets)
|
|
477
545
|
return console.warn("startup.toolsets provided but ignored in DYNAMIC mode"), { mode: "DYNAMIC" };
|
|
478
546
|
if (e.mode === "STATIC") {
|
|
479
547
|
if (e.toolsets === "ALL")
|
|
480
548
|
return { mode: "STATIC", toolsets: "ALL" };
|
|
481
|
-
const
|
|
482
|
-
for (const i of
|
|
483
|
-
const { isValid:
|
|
484
|
-
|
|
549
|
+
const t = Array.isArray(e.toolsets) ? e.toolsets : [], o = [];
|
|
550
|
+
for (const i of t) {
|
|
551
|
+
const { isValid: n, sanitized: l, error: a } = this.toolsetValidator.validateToolsetName(i, s);
|
|
552
|
+
n && l ? o.push(l) : a && console.warn(a);
|
|
485
553
|
}
|
|
486
|
-
if (
|
|
554
|
+
if (t.length > 0 && o.length === 0)
|
|
487
555
|
throw new Error(
|
|
488
556
|
"STATIC mode requires valid toolsets or 'ALL'; none were valid"
|
|
489
557
|
);
|
|
@@ -493,16 +561,16 @@ class y {
|
|
|
493
561
|
}
|
|
494
562
|
if (e.toolsets === "ALL") return { mode: "STATIC", toolsets: "ALL" };
|
|
495
563
|
if (Array.isArray(e.toolsets) && e.toolsets.length > 0) {
|
|
496
|
-
const
|
|
564
|
+
const t = [];
|
|
497
565
|
for (const o of e.toolsets) {
|
|
498
|
-
const { isValid: i, sanitized:
|
|
499
|
-
i &&
|
|
566
|
+
const { isValid: i, sanitized: n, error: l } = this.toolsetValidator.validateToolsetName(o, s);
|
|
567
|
+
i && n ? t.push(n) : l && console.warn(l);
|
|
500
568
|
}
|
|
501
|
-
if (
|
|
569
|
+
if (t.length === 0)
|
|
502
570
|
throw new Error(
|
|
503
571
|
"STATIC mode requires valid toolsets or 'ALL'; none were valid"
|
|
504
572
|
);
|
|
505
|
-
return { mode: "STATIC", toolsets:
|
|
573
|
+
return { mode: "STATIC", toolsets: t };
|
|
506
574
|
}
|
|
507
575
|
return { mode: "DYNAMIC" };
|
|
508
576
|
}
|
|
@@ -513,11 +581,13 @@ class y {
|
|
|
513
581
|
return this.manager;
|
|
514
582
|
}
|
|
515
583
|
}
|
|
516
|
-
|
|
584
|
+
var p, b;
|
|
585
|
+
class $ {
|
|
517
586
|
constructor(e = {}) {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
587
|
+
y(this, p);
|
|
588
|
+
this.storage = /* @__PURE__ */ new Map(), this.maxSize = e.maxSize ?? 1e3, this.ttlMs = e.ttlMs ?? 1e3 * 60 * 60, this.onEvict = e.onEvict;
|
|
589
|
+
const s = e.pruneIntervalMs ?? 1e3 * 60 * 10;
|
|
590
|
+
this.pruneInterval = setInterval(() => this.pruneExpired(), s);
|
|
521
591
|
}
|
|
522
592
|
getEntryCount() {
|
|
523
593
|
return this.storage.size;
|
|
@@ -529,47 +599,98 @@ class P {
|
|
|
529
599
|
return this.ttlMs;
|
|
530
600
|
}
|
|
531
601
|
get(e) {
|
|
532
|
-
const
|
|
533
|
-
return
|
|
602
|
+
const s = this.storage.get(e);
|
|
603
|
+
return s ? Date.now() - s.lastAccessed > this.ttlMs ? (this.delete(e), null) : (s.lastAccessed = Date.now(), this.storage.delete(e), this.storage.set(e, s), s.resource) : null;
|
|
534
604
|
}
|
|
535
|
-
set(e,
|
|
605
|
+
set(e, s) {
|
|
536
606
|
this.storage.size >= this.maxSize && this.evictLeastRecentlyUsed();
|
|
537
|
-
const
|
|
538
|
-
this.storage.set(e,
|
|
607
|
+
const t = { resource: s, lastAccessed: Date.now() };
|
|
608
|
+
this.storage.set(e, t);
|
|
539
609
|
}
|
|
610
|
+
/**
|
|
611
|
+
* Removes an entry from the cache.
|
|
612
|
+
* Calls the onEvict callback if configured.
|
|
613
|
+
* @param key - The key to remove
|
|
614
|
+
*/
|
|
540
615
|
delete(e) {
|
|
541
|
-
this.storage.
|
|
616
|
+
const s = this.storage.get(e);
|
|
617
|
+
s && (this.storage.delete(e), u(this, p, b).call(this, e, s.resource));
|
|
542
618
|
}
|
|
543
|
-
|
|
544
|
-
|
|
619
|
+
/**
|
|
620
|
+
* Stops the background pruning interval and optionally clears all entries.
|
|
621
|
+
* @param clearEntries - If true, also removes all entries and calls onEvict for each
|
|
622
|
+
*/
|
|
623
|
+
stop(e = !1) {
|
|
624
|
+
this.pruneInterval && (clearInterval(this.pruneInterval), this.pruneInterval = void 0), e && this.clear();
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Clears all entries from the cache.
|
|
628
|
+
* Calls onEvict for each entry being removed.
|
|
629
|
+
*/
|
|
630
|
+
clear() {
|
|
631
|
+
const e = Array.from(this.storage.entries());
|
|
632
|
+
this.storage.clear();
|
|
633
|
+
for (const [s, t] of e)
|
|
634
|
+
u(this, p, b).call(this, s, t.resource);
|
|
545
635
|
}
|
|
636
|
+
/**
|
|
637
|
+
* Evicts the least recently used entry from the cache.
|
|
638
|
+
* @private
|
|
639
|
+
*/
|
|
546
640
|
evictLeastRecentlyUsed() {
|
|
547
641
|
const e = this.storage.keys().next().value;
|
|
548
642
|
e && this.delete(e);
|
|
549
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* Removes all expired entries from the cache.
|
|
646
|
+
* @private
|
|
647
|
+
*/
|
|
550
648
|
pruneExpired() {
|
|
551
|
-
const e = Date.now();
|
|
552
|
-
for (const [t,
|
|
553
|
-
e -
|
|
649
|
+
const e = Date.now(), s = [];
|
|
650
|
+
for (const [t, o] of this.storage.entries())
|
|
651
|
+
e - o.lastAccessed > this.ttlMs && s.push(t);
|
|
652
|
+
for (const t of s)
|
|
653
|
+
this.delete(t);
|
|
554
654
|
}
|
|
555
655
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
656
|
+
p = new WeakSet(), /**
|
|
657
|
+
* Safely calls the evict callback, catching and logging any errors.
|
|
658
|
+
* @param key - The key being evicted
|
|
659
|
+
* @param resource - The resource being evicted
|
|
660
|
+
* @private
|
|
661
|
+
*/
|
|
662
|
+
b = function(e, s) {
|
|
663
|
+
if (this.onEvict)
|
|
664
|
+
try {
|
|
665
|
+
const t = this.onEvict(e, s);
|
|
666
|
+
t instanceof Promise && t.catch((o) => {
|
|
667
|
+
console.warn(`Error in cache eviction callback for key '${e}':`, o);
|
|
668
|
+
});
|
|
669
|
+
} catch (t) {
|
|
670
|
+
console.warn(`Error in cache eviction callback for key '${e}':`, t);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
class Q {
|
|
674
|
+
constructor(e, s, t = {}, o) {
|
|
675
|
+
this.app = null, this.clientCache = new $({
|
|
676
|
+
onEvict: (i, n) => {
|
|
677
|
+
this.cleanupBundle(n);
|
|
678
|
+
}
|
|
679
|
+
}), this.defaultManager = e, this.createBundle = s, this.options = {
|
|
680
|
+
host: t.host ?? "0.0.0.0",
|
|
681
|
+
port: t.port ?? 3e3,
|
|
682
|
+
basePath: t.basePath ?? "/",
|
|
683
|
+
cors: t.cors ?? !0,
|
|
684
|
+
logger: t.logger ?? !1,
|
|
685
|
+
app: t.app
|
|
565
686
|
}, this.configSchema = o;
|
|
566
687
|
}
|
|
567
688
|
async start() {
|
|
568
689
|
if (this.app) return;
|
|
569
|
-
const e = this.options.app ??
|
|
570
|
-
this.options.cors && await e.register(
|
|
571
|
-
const
|
|
572
|
-
e.get(`${
|
|
690
|
+
const e = this.options.app ?? M({ logger: this.options.logger });
|
|
691
|
+
this.options.cors && await e.register(x, { origin: !0 });
|
|
692
|
+
const s = this.options.basePath.endsWith("/") ? this.options.basePath.slice(0, -1) : this.options.basePath;
|
|
693
|
+
e.get(`${s}/healthz`, async () => ({ ok: !0 })), e.get(`${s}/tools`, async () => this.defaultManager.getStatus()), e.get(`${s}/.well-known/mcp-config`, async (t, o) => (o.header("Content-Type", "application/schema+json; charset=utf-8"), this.configSchema ?? {
|
|
573
694
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
574
695
|
title: "MCP Session Configuration",
|
|
575
696
|
description: "Schema for the /mcp endpoint configuration",
|
|
@@ -579,32 +700,32 @@ class J {
|
|
|
579
700
|
"x-mcp-version": "1.0",
|
|
580
701
|
"x-query-style": "dot+bracket"
|
|
581
702
|
})), e.post(
|
|
582
|
-
`${
|
|
583
|
-
async (
|
|
584
|
-
const i =
|
|
585
|
-
let
|
|
586
|
-
if (!
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
server:
|
|
590
|
-
orchestrator:
|
|
703
|
+
`${s}/mcp`,
|
|
704
|
+
async (t, o) => {
|
|
705
|
+
const i = t.headers["mcp-client-id"]?.trim(), n = i && i.length > 0 ? i : `anon-${v()}`, l = !n.startsWith("anon-");
|
|
706
|
+
let a = l ? this.clientCache.get(n) : null;
|
|
707
|
+
if (!a) {
|
|
708
|
+
const m = this.createBundle(), g = m.sessions;
|
|
709
|
+
a = {
|
|
710
|
+
server: m.server,
|
|
711
|
+
orchestrator: m.orchestrator,
|
|
591
712
|
sessions: g instanceof Map ? g : /* @__PURE__ */ new Map()
|
|
592
|
-
}, l && this.clientCache.set(
|
|
713
|
+
}, l && this.clientCache.set(n, a);
|
|
593
714
|
}
|
|
594
|
-
const c =
|
|
595
|
-
let
|
|
596
|
-
if (c &&
|
|
597
|
-
|
|
598
|
-
else if (!c &&
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
sessionIdGenerator: () =>
|
|
715
|
+
const c = t.headers["mcp-session-id"];
|
|
716
|
+
let d;
|
|
717
|
+
if (c && a.sessions.get(c))
|
|
718
|
+
d = a.sessions.get(c);
|
|
719
|
+
else if (!c && I(t.body)) {
|
|
720
|
+
const m = v();
|
|
721
|
+
d = new E({
|
|
722
|
+
sessionIdGenerator: () => m,
|
|
602
723
|
onsessioninitialized: (g) => {
|
|
603
|
-
|
|
724
|
+
a.sessions.set(g, d);
|
|
604
725
|
}
|
|
605
726
|
});
|
|
606
727
|
try {
|
|
607
|
-
await
|
|
728
|
+
await a.server.connect(d);
|
|
608
729
|
} catch {
|
|
609
730
|
return o.code(500), {
|
|
610
731
|
jsonrpc: "2.0",
|
|
@@ -612,8 +733,8 @@ class J {
|
|
|
612
733
|
id: null
|
|
613
734
|
};
|
|
614
735
|
}
|
|
615
|
-
|
|
616
|
-
|
|
736
|
+
d.onclose = () => {
|
|
737
|
+
d?.sessionId && a.sessions.delete(d.sessionId);
|
|
617
738
|
};
|
|
618
739
|
} else
|
|
619
740
|
return o.code(400), {
|
|
@@ -621,29 +742,29 @@ class J {
|
|
|
621
742
|
error: { code: -32e3, message: "Session not found or expired" },
|
|
622
743
|
id: null
|
|
623
744
|
};
|
|
624
|
-
return await
|
|
625
|
-
|
|
745
|
+
return await d.handleRequest(
|
|
746
|
+
t.raw,
|
|
626
747
|
o.raw,
|
|
627
|
-
|
|
748
|
+
t.body
|
|
628
749
|
), o;
|
|
629
750
|
}
|
|
630
|
-
), e.get(`${
|
|
631
|
-
const i =
|
|
632
|
-
if (!
|
|
751
|
+
), e.get(`${s}/mcp`, async (t, o) => {
|
|
752
|
+
const i = t.headers["mcp-client-id"]?.trim(), n = i && i.length > 0 ? i : "";
|
|
753
|
+
if (!n)
|
|
633
754
|
return o.code(400), "Missing mcp-client-id";
|
|
634
|
-
const l = this.clientCache.get(
|
|
755
|
+
const l = this.clientCache.get(n);
|
|
635
756
|
if (!l)
|
|
636
757
|
return o.code(400), "Invalid or expired client";
|
|
637
|
-
const
|
|
638
|
-
if (!
|
|
758
|
+
const a = t.headers["mcp-session-id"];
|
|
759
|
+
if (!a)
|
|
639
760
|
return o.code(400), "Missing mcp-session-id";
|
|
640
|
-
const c = l.sessions.get(
|
|
641
|
-
return c ? (await c.handleRequest(
|
|
761
|
+
const c = l.sessions.get(a);
|
|
762
|
+
return c ? (await c.handleRequest(t.raw, o.raw), o) : (o.code(400), "Invalid or expired session ID");
|
|
642
763
|
}), e.delete(
|
|
643
|
-
`${
|
|
644
|
-
async (
|
|
645
|
-
const i =
|
|
646
|
-
if (!
|
|
764
|
+
`${s}/mcp`,
|
|
765
|
+
async (t, o) => {
|
|
766
|
+
const i = t.headers["mcp-client-id"]?.trim(), n = i && i.length > 0 ? i : "", l = t.headers["mcp-session-id"];
|
|
767
|
+
if (!n || !l)
|
|
647
768
|
return o.code(400), {
|
|
648
769
|
jsonrpc: "2.0",
|
|
649
770
|
error: {
|
|
@@ -652,8 +773,8 @@ class J {
|
|
|
652
773
|
},
|
|
653
774
|
id: null
|
|
654
775
|
};
|
|
655
|
-
const
|
|
656
|
-
if (!
|
|
776
|
+
const a = this.clientCache.get(n), c = a?.sessions.get(l);
|
|
777
|
+
if (!a || !c)
|
|
657
778
|
return o.code(404), {
|
|
658
779
|
jsonrpc: "2.0",
|
|
659
780
|
error: { code: -32e3, message: "Session not found or expired" },
|
|
@@ -666,62 +787,84 @@ class J {
|
|
|
666
787
|
} catch {
|
|
667
788
|
}
|
|
668
789
|
} finally {
|
|
669
|
-
c?.sessionId ?
|
|
790
|
+
c?.sessionId ? a.sessions.delete(c.sessionId) : a.sessions.delete(l);
|
|
670
791
|
}
|
|
671
792
|
return o.code(204).send(), o;
|
|
672
793
|
}
|
|
673
794
|
), this.options.app || await e.listen({ host: this.options.host, port: this.options.port }), this.app = e;
|
|
674
795
|
}
|
|
796
|
+
/**
|
|
797
|
+
* Stops the Fastify server and cleans up all resources.
|
|
798
|
+
* Closes all client sessions and clears the cache.
|
|
799
|
+
*/
|
|
675
800
|
async stop() {
|
|
676
|
-
this.app && (this.options.app || await this.app.close(), this.app = null);
|
|
801
|
+
this.app && (this.clientCache.stop(!0), this.options.app || await this.app.close(), this.app = null);
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Cleans up resources associated with a client bundle.
|
|
805
|
+
* Closes all sessions within the bundle.
|
|
806
|
+
* @param bundle - The client bundle to clean up
|
|
807
|
+
* @private
|
|
808
|
+
*/
|
|
809
|
+
cleanupBundle(e) {
|
|
810
|
+
for (const [s, t] of e.sessions.entries())
|
|
811
|
+
try {
|
|
812
|
+
typeof t.close == "function" && t.close().catch((o) => {
|
|
813
|
+
console.warn(`Error closing session ${s}:`, o);
|
|
814
|
+
});
|
|
815
|
+
} catch (o) {
|
|
816
|
+
console.warn(`Error closing session ${s}:`, o);
|
|
817
|
+
}
|
|
818
|
+
e.sessions.clear();
|
|
677
819
|
}
|
|
678
820
|
}
|
|
679
|
-
async function
|
|
821
|
+
async function ge(r) {
|
|
680
822
|
const e = r.startup?.mode ?? "DYNAMIC";
|
|
681
823
|
if (typeof r.createServer != "function")
|
|
682
824
|
throw new Error("createMcpServer: `createServer` (factory) is required");
|
|
683
|
-
const
|
|
825
|
+
const s = r.createServer(), t = (a) => typeof a?.server?.notification == "function", o = (a) => typeof a?.notifyToolsListChanged == "function", i = async (a) => {
|
|
684
826
|
try {
|
|
685
|
-
if (
|
|
686
|
-
await
|
|
827
|
+
if (t(a)) {
|
|
828
|
+
await a.server.notification({
|
|
687
829
|
method: "notifications/tools/list_changed"
|
|
688
830
|
});
|
|
689
831
|
return;
|
|
690
832
|
}
|
|
691
|
-
o(
|
|
692
|
-
} catch {
|
|
833
|
+
o(a) && await a.notifyToolsListChanged();
|
|
834
|
+
} catch (c) {
|
|
835
|
+
console.warn("Failed to send tools list changed notification:", c);
|
|
693
836
|
}
|
|
694
|
-
},
|
|
695
|
-
server:
|
|
837
|
+
}, n = new T({
|
|
838
|
+
server: s,
|
|
696
839
|
catalog: r.catalog,
|
|
697
840
|
moduleLoaders: r.moduleLoaders,
|
|
698
841
|
exposurePolicy: r.exposurePolicy,
|
|
699
842
|
context: r.context,
|
|
700
|
-
notifyToolsListChanged: async () => i(
|
|
843
|
+
notifyToolsListChanged: async () => i(s),
|
|
701
844
|
startup: r.startup,
|
|
702
845
|
registerMetaTools: r.registerMetaTools !== void 0 ? r.registerMetaTools : e === "DYNAMIC"
|
|
703
|
-
}), l = new
|
|
704
|
-
|
|
846
|
+
}), l = new Q(
|
|
847
|
+
n.getManager(),
|
|
705
848
|
() => {
|
|
706
849
|
if (e === "STATIC")
|
|
707
|
-
return { server:
|
|
708
|
-
const
|
|
709
|
-
server:
|
|
850
|
+
return { server: s, orchestrator: n };
|
|
851
|
+
const a = r.createServer(), c = new T({
|
|
852
|
+
server: a,
|
|
710
853
|
catalog: r.catalog,
|
|
711
854
|
moduleLoaders: r.moduleLoaders,
|
|
712
855
|
exposurePolicy: r.exposurePolicy,
|
|
713
856
|
context: r.context,
|
|
714
|
-
notifyToolsListChanged: async () => i(
|
|
857
|
+
notifyToolsListChanged: async () => i(a),
|
|
715
858
|
startup: r.startup,
|
|
716
859
|
registerMetaTools: r.registerMetaTools !== void 0 ? r.registerMetaTools : e === "DYNAMIC"
|
|
717
860
|
});
|
|
718
|
-
return { server:
|
|
861
|
+
return { server: a, orchestrator: c };
|
|
719
862
|
},
|
|
720
863
|
r.http,
|
|
721
864
|
r.configSchema
|
|
722
865
|
);
|
|
723
866
|
return {
|
|
724
|
-
server:
|
|
867
|
+
server: s,
|
|
725
868
|
start: async () => {
|
|
726
869
|
await l.start();
|
|
727
870
|
},
|
|
@@ -730,16 +873,16 @@ async function de(r) {
|
|
|
730
873
|
}
|
|
731
874
|
};
|
|
732
875
|
}
|
|
733
|
-
function
|
|
734
|
-
|
|
876
|
+
function X(r) {
|
|
877
|
+
Z(r), ee(r), se(r), te(r);
|
|
735
878
|
}
|
|
736
|
-
function
|
|
879
|
+
function Z(r) {
|
|
737
880
|
if (!r || typeof r != "object")
|
|
738
881
|
throw new Error(
|
|
739
882
|
"Permission configuration is required for createPermissionBasedMcpServer"
|
|
740
883
|
);
|
|
741
884
|
}
|
|
742
|
-
function
|
|
885
|
+
function ee(r) {
|
|
743
886
|
if (!r.source)
|
|
744
887
|
throw new Error('Permission source must be either "headers" or "config"');
|
|
745
888
|
if (r.source !== "headers" && r.source !== "config")
|
|
@@ -747,19 +890,19 @@ function K(r) {
|
|
|
747
890
|
`Invalid permission source: "${r.source}". Must be either "headers" or "config"`
|
|
748
891
|
);
|
|
749
892
|
}
|
|
750
|
-
function
|
|
893
|
+
function se(r) {
|
|
751
894
|
if (r.source === "config" && !r.staticMap && !r.resolver)
|
|
752
895
|
throw new Error(
|
|
753
896
|
"Config-based permissions require at least one of: staticMap or resolver function"
|
|
754
897
|
);
|
|
755
898
|
}
|
|
756
|
-
function
|
|
899
|
+
function te(r) {
|
|
757
900
|
if (r.staticMap !== void 0) {
|
|
758
901
|
if (typeof r.staticMap != "object" || r.staticMap === null)
|
|
759
902
|
throw new Error(
|
|
760
903
|
"staticMap must be an object mapping client IDs to toolset arrays"
|
|
761
904
|
);
|
|
762
|
-
|
|
905
|
+
oe(r.staticMap);
|
|
763
906
|
}
|
|
764
907
|
if (r.resolver !== void 0 && typeof r.resolver != "function")
|
|
765
908
|
throw new Error(
|
|
@@ -770,48 +913,61 @@ function X(r) {
|
|
|
770
913
|
if (r.headerName !== void 0 && (typeof r.headerName != "string" || r.headerName.length === 0))
|
|
771
914
|
throw new Error("headerName must be a non-empty string");
|
|
772
915
|
}
|
|
773
|
-
function
|
|
774
|
-
for (const [e,
|
|
775
|
-
if (!Array.isArray(
|
|
916
|
+
function oe(r) {
|
|
917
|
+
for (const [e, s] of Object.entries(r))
|
|
918
|
+
if (!Array.isArray(s))
|
|
776
919
|
throw new Error(
|
|
777
920
|
`staticMap value for client "${e}" must be an array of toolset names`
|
|
778
921
|
);
|
|
779
922
|
}
|
|
780
|
-
var
|
|
781
|
-
class
|
|
923
|
+
var f, L, j, N, k, D;
|
|
924
|
+
class re {
|
|
782
925
|
/**
|
|
783
926
|
* Creates a new PermissionResolver instance.
|
|
784
927
|
* @param config - The permission configuration defining how permissions are resolved
|
|
785
928
|
*/
|
|
786
929
|
constructor(e) {
|
|
787
|
-
|
|
788
|
-
this.config = e, this.cache = /* @__PURE__ */ new Map();
|
|
930
|
+
y(this, f);
|
|
931
|
+
this.config = e, this.cache = /* @__PURE__ */ new Map(), this.normalizedHeaderName = (e.headerName || "mcp-toolset-permissions").toLowerCase();
|
|
789
932
|
}
|
|
790
933
|
/**
|
|
791
934
|
* Resolves permissions for a client based on the configured source.
|
|
792
935
|
* Results are cached to improve performance for subsequent requests from the same client.
|
|
793
936
|
* Handles all errors gracefully by returning empty permissions on failure.
|
|
937
|
+
*
|
|
938
|
+
* Note on caching: For header-based permissions, permissions are cached by clientId.
|
|
939
|
+
* This means subsequent requests from the same client will use cached permissions,
|
|
940
|
+
* even if headers change. Use invalidateCache(clientId) to force re-resolution.
|
|
941
|
+
*
|
|
794
942
|
* @param clientId - The unique identifier for the client
|
|
795
943
|
* @param headers - Optional request headers (required for header-based permissions)
|
|
796
944
|
* @returns Array of toolset names the client is allowed to access
|
|
797
945
|
*/
|
|
798
|
-
resolvePermissions(e,
|
|
946
|
+
resolvePermissions(e, s) {
|
|
799
947
|
if (this.cache.has(e))
|
|
800
948
|
return this.cache.get(e);
|
|
801
|
-
let
|
|
949
|
+
let t;
|
|
802
950
|
try {
|
|
803
|
-
this.config.source === "headers" ?
|
|
951
|
+
this.config.source === "headers" ? t = u(this, f, L).call(this, s) : t = u(this, f, N).call(this, e), Array.isArray(t) || (console.warn(
|
|
804
952
|
`Permission resolution returned non-array for client ${e}, using empty permissions`
|
|
805
|
-
),
|
|
953
|
+
), t = []), t = t.filter(
|
|
806
954
|
(o) => typeof o == "string" && o.trim().length > 0
|
|
807
955
|
);
|
|
808
956
|
} catch (o) {
|
|
809
957
|
console.error(
|
|
810
958
|
`Unexpected error resolving permissions for client ${e}:`,
|
|
811
959
|
o
|
|
812
|
-
),
|
|
960
|
+
), t = [];
|
|
813
961
|
}
|
|
814
|
-
return this.cache.set(e,
|
|
962
|
+
return this.cache.set(e, t), t;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Invalidates cached permissions for a specific client.
|
|
966
|
+
* Call this when you know a client's permissions have changed.
|
|
967
|
+
* @param clientId - The client ID to invalidate
|
|
968
|
+
*/
|
|
969
|
+
invalidateCache(e) {
|
|
970
|
+
this.cache.delete(e);
|
|
815
971
|
}
|
|
816
972
|
/**
|
|
817
973
|
* Clears the permission cache.
|
|
@@ -821,26 +977,43 @@ class ee {
|
|
|
821
977
|
this.cache.clear();
|
|
822
978
|
}
|
|
823
979
|
}
|
|
824
|
-
|
|
980
|
+
f = new WeakSet(), /**
|
|
825
981
|
* Parses permissions from request headers.
|
|
826
982
|
* Extracts comma-separated toolset names from the configured header.
|
|
827
983
|
* Handles malformed headers gracefully by returning empty permissions.
|
|
984
|
+
* Uses case-insensitive header lookup per RFC 7230.
|
|
828
985
|
* @param headers - Request headers containing permission data
|
|
829
986
|
* @returns Array of toolset names from headers, or empty array if header is missing/malformed
|
|
830
987
|
* @private
|
|
831
988
|
*/
|
|
832
|
-
|
|
833
|
-
|
|
989
|
+
L = function(e) {
|
|
990
|
+
if (!e)
|
|
991
|
+
return [];
|
|
992
|
+
const s = u(this, f, j).call(this, e, this.normalizedHeaderName);
|
|
834
993
|
if (!s)
|
|
835
994
|
return [];
|
|
836
995
|
try {
|
|
837
|
-
return s.split(",").map((
|
|
838
|
-
} catch (
|
|
996
|
+
return s.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
997
|
+
} catch (t) {
|
|
839
998
|
return console.warn(
|
|
840
|
-
`Failed to parse permission header '${
|
|
841
|
-
|
|
999
|
+
`Failed to parse permission header '${this.normalizedHeaderName}':`,
|
|
1000
|
+
t
|
|
842
1001
|
), [];
|
|
843
1002
|
}
|
|
1003
|
+
}, /**
|
|
1004
|
+
* Finds a header value using case-insensitive key matching.
|
|
1005
|
+
* HTTP headers are case-insensitive per RFC 7230.
|
|
1006
|
+
* @param headers - The headers object to search
|
|
1007
|
+
* @param normalizedKey - The lowercase key to search for
|
|
1008
|
+
* @returns The header value if found, undefined otherwise
|
|
1009
|
+
* @private
|
|
1010
|
+
*/
|
|
1011
|
+
j = function(e, s) {
|
|
1012
|
+
if (e[s] !== void 0)
|
|
1013
|
+
return e[s];
|
|
1014
|
+
for (const [t, o] of Object.entries(e))
|
|
1015
|
+
if (t.toLowerCase() === s)
|
|
1016
|
+
return o;
|
|
844
1017
|
}, /**
|
|
845
1018
|
* Resolves permissions from server-side configuration.
|
|
846
1019
|
* Tries resolver function first (if provided), then falls back to static map,
|
|
@@ -849,16 +1022,16 @@ $ = function(e) {
|
|
|
849
1022
|
* @returns Array of toolset names from configuration
|
|
850
1023
|
* @private
|
|
851
1024
|
*/
|
|
852
|
-
|
|
1025
|
+
N = function(e) {
|
|
853
1026
|
if (this.config.resolver) {
|
|
854
|
-
const
|
|
855
|
-
if (
|
|
856
|
-
return
|
|
1027
|
+
const s = u(this, f, k).call(this, e);
|
|
1028
|
+
if (s !== null)
|
|
1029
|
+
return s;
|
|
857
1030
|
}
|
|
858
1031
|
if (this.config.staticMap) {
|
|
859
|
-
const
|
|
860
|
-
if (
|
|
861
|
-
return
|
|
1032
|
+
const s = u(this, f, D).call(this, e);
|
|
1033
|
+
if (s !== null)
|
|
1034
|
+
return s;
|
|
862
1035
|
}
|
|
863
1036
|
return this.config.defaultPermissions || [];
|
|
864
1037
|
}, /**
|
|
@@ -868,16 +1041,16 @@ E = function(e) {
|
|
|
868
1041
|
* @returns Array of toolset names if successful, null if resolver fails or returns invalid data
|
|
869
1042
|
* @private
|
|
870
1043
|
*/
|
|
871
|
-
|
|
1044
|
+
k = function(e) {
|
|
872
1045
|
try {
|
|
873
|
-
const
|
|
874
|
-
return Array.isArray(
|
|
1046
|
+
const s = this.config.resolver(e);
|
|
1047
|
+
return Array.isArray(s) ? s : (console.warn(
|
|
875
1048
|
`Permission resolver returned non-array for client ${e}, using fallback`
|
|
876
1049
|
), null);
|
|
877
|
-
} catch (
|
|
878
|
-
const
|
|
1050
|
+
} catch (s) {
|
|
1051
|
+
const t = s instanceof Error ? s.message : String(s);
|
|
879
1052
|
return console.warn(
|
|
880
|
-
`Permission resolver declined client ${e} (${
|
|
1053
|
+
`Permission resolver declined client ${e} (${t}), trying fallback`
|
|
881
1054
|
), null;
|
|
882
1055
|
}
|
|
883
1056
|
}, /**
|
|
@@ -887,25 +1060,37 @@ L = function(e) {
|
|
|
887
1060
|
* @returns Array of toolset names if found, null if client not in map
|
|
888
1061
|
* @private
|
|
889
1062
|
*/
|
|
890
|
-
|
|
891
|
-
const
|
|
892
|
-
return
|
|
1063
|
+
D = function(e) {
|
|
1064
|
+
const s = this.config.staticMap[e];
|
|
1065
|
+
return s !== void 0 ? Array.isArray(s) ? s : [] : null;
|
|
893
1066
|
};
|
|
894
|
-
function
|
|
895
|
-
return async (
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
), o = r(
|
|
900
|
-
|
|
1067
|
+
function ie(r, e) {
|
|
1068
|
+
return async (s) => {
|
|
1069
|
+
const t = e.resolvePermissions(
|
|
1070
|
+
s.clientId,
|
|
1071
|
+
s.headers
|
|
1072
|
+
), o = r(t), i = o.orchestrator.getManager(), n = [], l = [];
|
|
1073
|
+
if (t.length > 0) {
|
|
1074
|
+
const a = await i.enableToolsets(t);
|
|
1075
|
+
for (const c of a.results)
|
|
1076
|
+
c.success ? n.push(c.name) : (l.push(c.name), console.warn(
|
|
1077
|
+
`Failed to enable toolset '${c.name}' for client '${s.clientId}': ${c.message}`
|
|
1078
|
+
));
|
|
1079
|
+
if (n.length === 0 && l.length > 0)
|
|
1080
|
+
throw new Error(
|
|
1081
|
+
`All requested toolsets failed to enable for client '${s.clientId}'. Requested: [${t.join(", ")}]. Check that toolset names in permissions match the catalog.`
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
return {
|
|
901
1085
|
server: o.server,
|
|
902
1086
|
orchestrator: o.orchestrator,
|
|
903
|
-
allowedToolsets:
|
|
1087
|
+
allowedToolsets: n,
|
|
1088
|
+
failedToolsets: l
|
|
904
1089
|
};
|
|
905
1090
|
};
|
|
906
1091
|
}
|
|
907
|
-
var
|
|
908
|
-
class
|
|
1092
|
+
var h, z, R, O, F, V, _, B, Y, H, U;
|
|
1093
|
+
class ne {
|
|
909
1094
|
/**
|
|
910
1095
|
* Creates a new PermissionAwareFastifyTransport instance.
|
|
911
1096
|
* @param defaultManager - Default tool manager for status endpoints
|
|
@@ -913,15 +1098,19 @@ class te {
|
|
|
913
1098
|
* @param options - Transport configuration options
|
|
914
1099
|
* @param configSchema - Optional JSON schema for configuration discovery
|
|
915
1100
|
*/
|
|
916
|
-
constructor(e,
|
|
917
|
-
|
|
918
|
-
this.app = null, this.clientCache = new
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1101
|
+
constructor(e, s, t = {}, o) {
|
|
1102
|
+
y(this, h);
|
|
1103
|
+
this.app = null, this.clientCache = new $({
|
|
1104
|
+
onEvict: (i, n) => {
|
|
1105
|
+
u(this, h, z).call(this, n);
|
|
1106
|
+
}
|
|
1107
|
+
}), this.defaultManager = e, this.createPermissionAwareBundle = s, this.options = {
|
|
1108
|
+
host: t.host ?? "0.0.0.0",
|
|
1109
|
+
port: t.port ?? 3e3,
|
|
1110
|
+
basePath: t.basePath ?? "/",
|
|
1111
|
+
cors: t.cors ?? !0,
|
|
1112
|
+
logger: t.logger ?? !1,
|
|
1113
|
+
app: t.app
|
|
925
1114
|
}, this.configSchema = o;
|
|
926
1115
|
}
|
|
927
1116
|
/**
|
|
@@ -930,25 +1119,42 @@ class te {
|
|
|
930
1119
|
*/
|
|
931
1120
|
async start() {
|
|
932
1121
|
if (this.app) return;
|
|
933
|
-
const e = this.options.app ??
|
|
934
|
-
this.options.cors && await e.register(
|
|
935
|
-
const
|
|
936
|
-
u(this,
|
|
1122
|
+
const e = this.options.app ?? M({ logger: this.options.logger });
|
|
1123
|
+
this.options.cors && await e.register(x, { origin: !0 });
|
|
1124
|
+
const s = u(this, h, R).call(this, this.options.basePath);
|
|
1125
|
+
u(this, h, O).call(this, e, s), u(this, h, F).call(this, e, s), u(this, h, V).call(this, e, s), u(this, h, _).call(this, e, s), u(this, h, B).call(this, e, s), u(this, h, Y).call(this, e, s), this.options.app || await e.listen({ host: this.options.host, port: this.options.port }), this.app = e;
|
|
937
1126
|
}
|
|
938
1127
|
/**
|
|
939
|
-
* Stops the Fastify server and cleans up resources.
|
|
1128
|
+
* Stops the Fastify server and cleans up all resources.
|
|
1129
|
+
* Closes all client sessions and clears the cache.
|
|
940
1130
|
*/
|
|
941
1131
|
async stop() {
|
|
942
|
-
this.app && (this.options.app || await this.app.close(), this.app = null);
|
|
1132
|
+
this.app && (this.clientCache.stop(!0), this.options.app || await this.app.close(), this.app = null);
|
|
943
1133
|
}
|
|
944
1134
|
}
|
|
945
|
-
|
|
1135
|
+
h = new WeakSet(), /**
|
|
1136
|
+
* Cleans up resources associated with a client bundle.
|
|
1137
|
+
* Closes all sessions within the bundle.
|
|
1138
|
+
* @param bundle - The client bundle to clean up
|
|
1139
|
+
* @private
|
|
1140
|
+
*/
|
|
1141
|
+
z = function(e) {
|
|
1142
|
+
for (const [s, t] of e.sessions.entries())
|
|
1143
|
+
try {
|
|
1144
|
+
typeof t.close == "function" && t.close().catch((o) => {
|
|
1145
|
+
console.warn(`Error closing session ${s}:`, o);
|
|
1146
|
+
});
|
|
1147
|
+
} catch (o) {
|
|
1148
|
+
console.warn(`Error closing session ${s}:`, o);
|
|
1149
|
+
}
|
|
1150
|
+
e.sessions.clear();
|
|
1151
|
+
}, /**
|
|
946
1152
|
* Normalizes the base path by removing trailing slashes.
|
|
947
1153
|
* @param basePath - The base path to normalize
|
|
948
1154
|
* @returns Normalized base path without trailing slash
|
|
949
1155
|
* @private
|
|
950
1156
|
*/
|
|
951
|
-
|
|
1157
|
+
R = function(e) {
|
|
952
1158
|
return e.endsWith("/") ? e.slice(0, -1) : e;
|
|
953
1159
|
}, /**
|
|
954
1160
|
* Registers the health check endpoint.
|
|
@@ -956,24 +1162,24 @@ D = function(e) {
|
|
|
956
1162
|
* @param base - Base path for routes
|
|
957
1163
|
* @private
|
|
958
1164
|
*/
|
|
959
|
-
|
|
960
|
-
e.get(`${
|
|
1165
|
+
O = function(e, s) {
|
|
1166
|
+
e.get(`${s}/healthz`, async () => ({ ok: !0 }));
|
|
961
1167
|
}, /**
|
|
962
1168
|
* Registers the tools status endpoint.
|
|
963
1169
|
* @param app - Fastify instance
|
|
964
1170
|
* @param base - Base path for routes
|
|
965
1171
|
* @private
|
|
966
1172
|
*/
|
|
967
|
-
|
|
968
|
-
e.get(`${
|
|
1173
|
+
F = function(e, s) {
|
|
1174
|
+
e.get(`${s}/tools`, async () => this.defaultManager.getStatus());
|
|
969
1175
|
}, /**
|
|
970
1176
|
* Registers the MCP configuration discovery endpoint.
|
|
971
1177
|
* @param app - Fastify instance
|
|
972
1178
|
* @param base - Base path for routes
|
|
973
1179
|
* @private
|
|
974
1180
|
*/
|
|
975
|
-
|
|
976
|
-
e.get(`${
|
|
1181
|
+
V = function(e, s) {
|
|
1182
|
+
e.get(`${s}/.well-known/mcp-config`, async (t, o) => (o.header("Content-Type", "application/schema+json; charset=utf-8"), this.configSchema ?? {
|
|
977
1183
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
978
1184
|
title: "MCP Session Configuration",
|
|
979
1185
|
description: "Schema for the /mcp endpoint configuration",
|
|
@@ -990,37 +1196,42 @@ O = function(e, t) {
|
|
|
990
1196
|
* @param base - Base path for routes
|
|
991
1197
|
* @private
|
|
992
1198
|
*/
|
|
993
|
-
|
|
1199
|
+
_ = function(e, s) {
|
|
994
1200
|
e.post(
|
|
995
|
-
`${
|
|
996
|
-
async (
|
|
997
|
-
const i = u(this,
|
|
998
|
-
let l =
|
|
1201
|
+
`${s}/mcp`,
|
|
1202
|
+
async (t, o) => {
|
|
1203
|
+
const i = u(this, h, H).call(this, t), n = !i.clientId.startsWith("anon-");
|
|
1204
|
+
let l = n ? this.clientCache.get(i.clientId) : null;
|
|
999
1205
|
if (!l)
|
|
1000
1206
|
try {
|
|
1001
|
-
const
|
|
1207
|
+
const d = await this.createPermissionAwareBundle(i);
|
|
1208
|
+
d.failedToolsets.length > 0 && console.warn(
|
|
1209
|
+
`Client ${i.clientId} had ${d.failedToolsets.length} toolsets fail to enable: [${d.failedToolsets.join(", ")}]. Successfully enabled: [${d.allowedToolsets.join(", ")}]`
|
|
1210
|
+
);
|
|
1211
|
+
const m = d.sessions;
|
|
1002
1212
|
l = {
|
|
1003
|
-
server:
|
|
1004
|
-
orchestrator:
|
|
1005
|
-
allowedToolsets:
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1213
|
+
server: d.server,
|
|
1214
|
+
orchestrator: d.orchestrator,
|
|
1215
|
+
allowedToolsets: d.allowedToolsets,
|
|
1216
|
+
failedToolsets: d.failedToolsets,
|
|
1217
|
+
sessions: m instanceof Map ? m : /* @__PURE__ */ new Map()
|
|
1218
|
+
}, n && this.clientCache.set(i.clientId, l);
|
|
1219
|
+
} catch (d) {
|
|
1009
1220
|
return console.error(
|
|
1010
1221
|
`Failed to create permission-aware bundle for client ${i.clientId}:`,
|
|
1011
|
-
|
|
1012
|
-
), o.code(403), u(this,
|
|
1222
|
+
d
|
|
1223
|
+
), o.code(403), u(this, h, U).call(this, "Access denied");
|
|
1013
1224
|
}
|
|
1014
|
-
const
|
|
1225
|
+
const a = t.headers["mcp-session-id"];
|
|
1015
1226
|
let c;
|
|
1016
|
-
if (
|
|
1017
|
-
c = l.sessions.get(
|
|
1018
|
-
else if (!
|
|
1019
|
-
const
|
|
1020
|
-
c = new
|
|
1021
|
-
sessionIdGenerator: () =>
|
|
1022
|
-
onsessioninitialized: (
|
|
1023
|
-
l.sessions.set(
|
|
1227
|
+
if (a && l.sessions.get(a))
|
|
1228
|
+
c = l.sessions.get(a);
|
|
1229
|
+
else if (!a && I(t.body)) {
|
|
1230
|
+
const d = v();
|
|
1231
|
+
c = new E({
|
|
1232
|
+
sessionIdGenerator: () => d,
|
|
1233
|
+
onsessioninitialized: (m) => {
|
|
1234
|
+
l.sessions.set(m, c);
|
|
1024
1235
|
}
|
|
1025
1236
|
});
|
|
1026
1237
|
try {
|
|
@@ -1042,9 +1253,9 @@ R = function(e, t) {
|
|
|
1042
1253
|
id: null
|
|
1043
1254
|
};
|
|
1044
1255
|
return await c.handleRequest(
|
|
1045
|
-
|
|
1256
|
+
t.raw,
|
|
1046
1257
|
o.raw,
|
|
1047
|
-
|
|
1258
|
+
t.body
|
|
1048
1259
|
), o;
|
|
1049
1260
|
}
|
|
1050
1261
|
);
|
|
@@ -1054,19 +1265,19 @@ R = function(e, t) {
|
|
|
1054
1265
|
* @param base - Base path for routes
|
|
1055
1266
|
* @private
|
|
1056
1267
|
*/
|
|
1057
|
-
|
|
1058
|
-
e.get(`${
|
|
1059
|
-
const i =
|
|
1060
|
-
if (!
|
|
1268
|
+
B = function(e, s) {
|
|
1269
|
+
e.get(`${s}/mcp`, async (t, o) => {
|
|
1270
|
+
const i = t.headers["mcp-client-id"]?.trim(), n = i && i.length > 0 ? i : "";
|
|
1271
|
+
if (!n)
|
|
1061
1272
|
return o.code(400), "Missing mcp-client-id";
|
|
1062
|
-
const l = this.clientCache.get(
|
|
1273
|
+
const l = this.clientCache.get(n);
|
|
1063
1274
|
if (!l)
|
|
1064
1275
|
return o.code(400), "Invalid or expired client";
|
|
1065
|
-
const
|
|
1066
|
-
if (!
|
|
1276
|
+
const a = t.headers["mcp-session-id"];
|
|
1277
|
+
if (!a)
|
|
1067
1278
|
return o.code(400), "Missing mcp-session-id";
|
|
1068
|
-
const c = l.sessions.get(
|
|
1069
|
-
return c ? (await c.handleRequest(
|
|
1279
|
+
const c = l.sessions.get(a);
|
|
1280
|
+
return c ? (await c.handleRequest(t.raw, o.raw), o) : (o.code(400), "Invalid or expired session ID");
|
|
1070
1281
|
});
|
|
1071
1282
|
}, /**
|
|
1072
1283
|
* Registers the DELETE /mcp endpoint for session termination.
|
|
@@ -1074,12 +1285,12 @@ k = function(e, t) {
|
|
|
1074
1285
|
* @param base - Base path for routes
|
|
1075
1286
|
* @private
|
|
1076
1287
|
*/
|
|
1077
|
-
|
|
1288
|
+
Y = function(e, s) {
|
|
1078
1289
|
e.delete(
|
|
1079
|
-
`${
|
|
1080
|
-
async (
|
|
1081
|
-
const i =
|
|
1082
|
-
if (!
|
|
1290
|
+
`${s}/mcp`,
|
|
1291
|
+
async (t, o) => {
|
|
1292
|
+
const i = t.headers["mcp-client-id"]?.trim(), n = i && i.length > 0 ? i : "", l = t.headers["mcp-session-id"];
|
|
1293
|
+
if (!n || !l)
|
|
1083
1294
|
return o.code(400), {
|
|
1084
1295
|
jsonrpc: "2.0",
|
|
1085
1296
|
error: {
|
|
@@ -1088,8 +1299,8 @@ F = function(e, t) {
|
|
|
1088
1299
|
},
|
|
1089
1300
|
id: null
|
|
1090
1301
|
};
|
|
1091
|
-
const
|
|
1092
|
-
if (!
|
|
1302
|
+
const a = this.clientCache.get(n), c = a?.sessions.get(l);
|
|
1303
|
+
if (!a || !c)
|
|
1093
1304
|
return o.code(404), {
|
|
1094
1305
|
jsonrpc: "2.0",
|
|
1095
1306
|
error: { code: -32e3, message: "Session not found or expired" },
|
|
@@ -1102,7 +1313,7 @@ F = function(e, t) {
|
|
|
1102
1313
|
} catch {
|
|
1103
1314
|
}
|
|
1104
1315
|
} finally {
|
|
1105
|
-
c?.sessionId ?
|
|
1316
|
+
c?.sessionId ? a.sessions.delete(c.sessionId) : a.sessions.delete(l);
|
|
1106
1317
|
}
|
|
1107
1318
|
return o.code(204).send(), o;
|
|
1108
1319
|
}
|
|
@@ -1114,11 +1325,11 @@ F = function(e, t) {
|
|
|
1114
1325
|
* @returns Client request context with ID and headers
|
|
1115
1326
|
* @private
|
|
1116
1327
|
*/
|
|
1117
|
-
|
|
1118
|
-
const
|
|
1119
|
-
for (const [i,
|
|
1120
|
-
typeof
|
|
1121
|
-
return { clientId:
|
|
1328
|
+
H = function(e) {
|
|
1329
|
+
const s = e.headers["mcp-client-id"]?.trim(), t = s && s.length > 0 ? s : `anon-${v()}`, o = {};
|
|
1330
|
+
for (const [i, n] of Object.entries(e.headers))
|
|
1331
|
+
typeof n == "string" && (o[i] = n);
|
|
1332
|
+
return { clientId: t, headers: o };
|
|
1122
1333
|
}, /**
|
|
1123
1334
|
* Creates a safe error response that doesn't expose unauthorized toolset information.
|
|
1124
1335
|
* Used for permission-related errors to prevent information leakage.
|
|
@@ -1127,22 +1338,37 @@ V = function(e) {
|
|
|
1127
1338
|
* @returns JSON-RPC error response object
|
|
1128
1339
|
* @private
|
|
1129
1340
|
*/
|
|
1130
|
-
|
|
1341
|
+
U = function(e = "Access denied", s = -32e3) {
|
|
1131
1342
|
return {
|
|
1132
1343
|
jsonrpc: "2.0",
|
|
1133
1344
|
error: {
|
|
1134
|
-
code:
|
|
1345
|
+
code: s,
|
|
1135
1346
|
message: e
|
|
1136
1347
|
},
|
|
1137
1348
|
id: null
|
|
1138
1349
|
};
|
|
1139
1350
|
};
|
|
1140
|
-
|
|
1351
|
+
function ae(r) {
|
|
1352
|
+
if (!r) return;
|
|
1353
|
+
const e = {
|
|
1354
|
+
namespaceToolsWithSetKey: r.namespaceToolsWithSetKey
|
|
1355
|
+
};
|
|
1356
|
+
return r.allowlist !== void 0 && console.warn(
|
|
1357
|
+
"Permission-based servers: exposurePolicy.allowlist is ignored. Allowed toolsets are determined by client permissions."
|
|
1358
|
+
), r.denylist !== void 0 && console.warn(
|
|
1359
|
+
"Permission-based servers: exposurePolicy.denylist is ignored. Use permission configuration to control toolset access."
|
|
1360
|
+
), r.maxActiveToolsets !== void 0 && console.warn(
|
|
1361
|
+
"Permission-based servers: exposurePolicy.maxActiveToolsets is ignored. Toolset count is determined by client permissions."
|
|
1362
|
+
), r.onLimitExceeded !== void 0 && console.warn(
|
|
1363
|
+
"Permission-based servers: exposurePolicy.onLimitExceeded is ignored. No toolset limits are enforced."
|
|
1364
|
+
), e;
|
|
1365
|
+
}
|
|
1366
|
+
async function pe(r) {
|
|
1141
1367
|
if (!r.permissions)
|
|
1142
1368
|
throw new Error(
|
|
1143
1369
|
"Permission configuration is required for createPermissionBasedMcpServer. Please provide a 'permissions' field in the options."
|
|
1144
1370
|
);
|
|
1145
|
-
if (
|
|
1371
|
+
if (X(r.permissions), r.startup)
|
|
1146
1372
|
throw new Error(
|
|
1147
1373
|
"Permission-based servers determine toolsets from client permissions. The 'startup' option is not allowed. Remove it from your configuration."
|
|
1148
1374
|
);
|
|
@@ -1150,23 +1376,25 @@ async function he(r) {
|
|
|
1150
1376
|
throw new Error(
|
|
1151
1377
|
"createPermissionBasedMcpServer: `createServer` (factory) is required"
|
|
1152
1378
|
);
|
|
1153
|
-
const e =
|
|
1379
|
+
const e = ae(
|
|
1380
|
+
r.exposurePolicy
|
|
1381
|
+
), s = new re(r.permissions), t = r.createServer(), o = new T({
|
|
1154
1382
|
server: t,
|
|
1155
1383
|
catalog: r.catalog,
|
|
1156
1384
|
moduleLoaders: r.moduleLoaders,
|
|
1157
|
-
exposurePolicy:
|
|
1385
|
+
exposurePolicy: e,
|
|
1158
1386
|
context: r.context,
|
|
1159
1387
|
notifyToolsListChanged: void 0,
|
|
1160
1388
|
// No notifications in STATIC mode
|
|
1161
1389
|
startup: { mode: "STATIC", toolsets: [] },
|
|
1162
1390
|
registerMetaTools: !1
|
|
1163
|
-
}),
|
|
1164
|
-
(
|
|
1165
|
-
const
|
|
1166
|
-
server:
|
|
1391
|
+
}), i = ie(
|
|
1392
|
+
(l) => {
|
|
1393
|
+
const a = r.createServer(), c = new T({
|
|
1394
|
+
server: a,
|
|
1167
1395
|
catalog: r.catalog,
|
|
1168
1396
|
moduleLoaders: r.moduleLoaders,
|
|
1169
|
-
exposurePolicy:
|
|
1397
|
+
exposurePolicy: e,
|
|
1170
1398
|
context: r.context,
|
|
1171
1399
|
notifyToolsListChanged: void 0,
|
|
1172
1400
|
// No notifications in STATIC mode
|
|
@@ -1175,31 +1403,31 @@ async function he(r) {
|
|
|
1175
1403
|
registerMetaTools: !1
|
|
1176
1404
|
// No meta-tools - toolsets are fixed per client
|
|
1177
1405
|
});
|
|
1178
|
-
return { server:
|
|
1406
|
+
return { server: a, orchestrator: c };
|
|
1179
1407
|
},
|
|
1180
|
-
|
|
1181
|
-
),
|
|
1182
|
-
|
|
1183
|
-
|
|
1408
|
+
s
|
|
1409
|
+
), n = new ne(
|
|
1410
|
+
o.getManager(),
|
|
1411
|
+
i,
|
|
1184
1412
|
r.http,
|
|
1185
1413
|
r.configSchema
|
|
1186
1414
|
);
|
|
1187
1415
|
return {
|
|
1188
1416
|
server: t,
|
|
1189
1417
|
start: async () => {
|
|
1190
|
-
await
|
|
1418
|
+
await n.start();
|
|
1191
1419
|
},
|
|
1192
1420
|
close: async () => {
|
|
1193
1421
|
try {
|
|
1194
|
-
await
|
|
1422
|
+
await n.stop();
|
|
1195
1423
|
} finally {
|
|
1196
|
-
|
|
1424
|
+
s.clearCache();
|
|
1197
1425
|
}
|
|
1198
1426
|
}
|
|
1199
1427
|
};
|
|
1200
1428
|
}
|
|
1201
1429
|
export {
|
|
1202
|
-
|
|
1203
|
-
|
|
1430
|
+
ge as createMcpServer,
|
|
1431
|
+
pe as createPermissionBasedMcpServer
|
|
1204
1432
|
};
|
|
1205
1433
|
//# sourceMappingURL=index.js.map
|