toolception 0.2.5 → 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 +945 -21
- 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.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +940 -237
- 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 +47 -0
- package/dist/permissions/PermissionAwareFastifyTransport.d.ts.map +1 -0
- package/dist/permissions/PermissionResolver.d.ts +43 -0
- package/dist/permissions/PermissionResolver.d.ts.map +1 -0
- package/dist/permissions/createPermissionAwareBundle.d.ts +58 -0
- package/dist/permissions/createPermissionAwareBundle.d.ts.map +1 -0
- package/dist/permissions/validatePermissionConfig.d.ts +9 -0
- package/dist/permissions/validatePermissionConfig.d.ts.map +1 -0
- package/dist/server/createMcpServer.d.ts +4 -3
- package/dist/server/createMcpServer.d.ts.map +1 -1
- package/dist/server/createPermissionBasedMcpServer.d.ts +65 -0
- package/dist/server/createPermissionBasedMcpServer.d.ts.map +1 -0
- package/dist/session/ClientResourceCache.d.ts +34 -3
- package/dist/session/ClientResourceCache.d.ts.map +1 -1
- package/dist/types/index.d.ts +229 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -2
package/dist/index.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
var A = (r) => {
|
|
2
|
+
throw TypeError(r);
|
|
3
|
+
};
|
|
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 = {
|
|
8
14
|
dynamic: [
|
|
9
15
|
"dynamic-tool-discovery",
|
|
10
16
|
"dynamicToolDiscovery",
|
|
@@ -12,11 +18,11 @@ const g = {
|
|
|
12
18
|
],
|
|
13
19
|
toolsets: ["tool-sets", "toolSets", "FMP_TOOL_SETS"]
|
|
14
20
|
};
|
|
15
|
-
class
|
|
21
|
+
class q {
|
|
16
22
|
constructor(e = {}) {
|
|
17
23
|
this.keys = {
|
|
18
|
-
dynamic: e.keys?.dynamic ??
|
|
19
|
-
toolsets: e.keys?.toolsets ??
|
|
24
|
+
dynamic: e.keys?.dynamic ?? S.dynamic,
|
|
25
|
+
toolsets: e.keys?.toolsets ?? S.toolsets
|
|
20
26
|
};
|
|
21
27
|
}
|
|
22
28
|
resolveMode(e, s) {
|
|
@@ -24,20 +30,20 @@ class S {
|
|
|
24
30
|
}
|
|
25
31
|
parseCommaSeparatedToolSets(e, s) {
|
|
26
32
|
if (!e || typeof e != "string") return [];
|
|
27
|
-
const t = e.split(",").map((
|
|
28
|
-
for (const
|
|
29
|
-
o.has(
|
|
30
|
-
`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(
|
|
31
37
|
o
|
|
32
38
|
).join(", ")}`
|
|
33
39
|
);
|
|
34
|
-
return
|
|
40
|
+
return i;
|
|
35
41
|
}
|
|
36
42
|
getModulesForToolSets(e, s) {
|
|
37
43
|
const t = /* @__PURE__ */ new Set();
|
|
38
44
|
for (const o of e) {
|
|
39
|
-
const
|
|
40
|
-
|
|
45
|
+
const i = s[o];
|
|
46
|
+
i && (i.modules || []).forEach((n) => t.add(n));
|
|
41
47
|
}
|
|
42
48
|
return Array.from(t);
|
|
43
49
|
}
|
|
@@ -62,13 +68,22 @@ class S {
|
|
|
62
68
|
).join(", ")}`
|
|
63
69
|
};
|
|
64
70
|
}
|
|
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
|
+
*/
|
|
65
78
|
validateToolsetModules(e, s) {
|
|
66
79
|
try {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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) };
|
|
72
87
|
} catch (t) {
|
|
73
88
|
return {
|
|
74
89
|
isValid: !1,
|
|
@@ -94,7 +109,7 @@ class S {
|
|
|
94
109
|
}
|
|
95
110
|
}
|
|
96
111
|
}
|
|
97
|
-
class
|
|
112
|
+
class J {
|
|
98
113
|
constructor(e) {
|
|
99
114
|
this.catalog = e.catalog, this.moduleLoaders = e.moduleLoaders ?? {};
|
|
100
115
|
}
|
|
@@ -128,18 +143,18 @@ class x {
|
|
|
128
143
|
async resolveToolsForToolsets(e, s) {
|
|
129
144
|
const t = [];
|
|
130
145
|
for (const o of e) {
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
for (const
|
|
134
|
-
const l = this.moduleLoaders[
|
|
146
|
+
const i = this.catalog[o];
|
|
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];
|
|
135
150
|
if (l)
|
|
136
151
|
try {
|
|
137
|
-
const
|
|
138
|
-
Array.isArray(
|
|
139
|
-
} catch (
|
|
152
|
+
const a = await l(s);
|
|
153
|
+
Array.isArray(a) && a.length > 0 && t.push(...a);
|
|
154
|
+
} catch (a) {
|
|
140
155
|
console.warn(
|
|
141
|
-
`Module loader '${
|
|
142
|
-
|
|
156
|
+
`Module loader '${n}' failed for toolset '${o}':`,
|
|
157
|
+
a
|
|
143
158
|
);
|
|
144
159
|
}
|
|
145
160
|
}
|
|
@@ -147,12 +162,12 @@ class x {
|
|
|
147
162
|
return t;
|
|
148
163
|
}
|
|
149
164
|
}
|
|
150
|
-
class
|
|
165
|
+
class C extends Error {
|
|
151
166
|
constructor(e, s, t, o) {
|
|
152
167
|
super(e), this.name = "ToolingError", this.code = s, this.details = t;
|
|
153
168
|
}
|
|
154
169
|
}
|
|
155
|
-
class
|
|
170
|
+
class P {
|
|
156
171
|
constructor(e = {}) {
|
|
157
172
|
this.names = /* @__PURE__ */ new Set(), this.toolsetToNames = /* @__PURE__ */ new Map(), this.options = {
|
|
158
173
|
namespaceWithToolset: e.namespaceWithToolset ?? !0
|
|
@@ -166,7 +181,7 @@ class v {
|
|
|
166
181
|
}
|
|
167
182
|
add(e) {
|
|
168
183
|
if (this.names.has(e))
|
|
169
|
-
throw new
|
|
184
|
+
throw new C(
|
|
170
185
|
`Tool name collision: '${e}' already registered`,
|
|
171
186
|
"E_TOOL_NAME_CONFLICT"
|
|
172
187
|
);
|
|
@@ -181,7 +196,7 @@ class v {
|
|
|
181
196
|
return s.map((t) => {
|
|
182
197
|
const o = this.getSafeName(e, t.name);
|
|
183
198
|
if (this.has(o))
|
|
184
|
-
throw new
|
|
199
|
+
throw new C(
|
|
185
200
|
`Tool name collision for '${o}'`,
|
|
186
201
|
"E_TOOL_NAME_CONFLICT"
|
|
187
202
|
);
|
|
@@ -198,9 +213,23 @@ class v {
|
|
|
198
213
|
return e;
|
|
199
214
|
}
|
|
200
215
|
}
|
|
201
|
-
class
|
|
216
|
+
class G {
|
|
202
217
|
constructor(e) {
|
|
203
|
-
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
|
+
}
|
|
204
233
|
}
|
|
205
234
|
getAvailableToolsets() {
|
|
206
235
|
return this.resolver.getAvailableToolsets();
|
|
@@ -214,66 +243,97 @@ class C {
|
|
|
214
243
|
isActive(e) {
|
|
215
244
|
return this.activeToolsets.has(e);
|
|
216
245
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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) {
|
|
254
|
+
const t = this.resolver.validateToolsetName(e);
|
|
255
|
+
if (!t.isValid || !t.sanitized)
|
|
220
256
|
return {
|
|
221
257
|
success: !1,
|
|
222
|
-
message:
|
|
258
|
+
message: t.error || "Unknown validation error"
|
|
223
259
|
};
|
|
224
|
-
const
|
|
225
|
-
if (this.activeToolsets.has(
|
|
260
|
+
const o = t.sanitized;
|
|
261
|
+
if (this.activeToolsets.has(o))
|
|
226
262
|
return {
|
|
227
263
|
success: !1,
|
|
228
|
-
message: `Toolset '${
|
|
264
|
+
message: `Toolset '${o}' is already enabled.`
|
|
229
265
|
};
|
|
266
|
+
const i = this.checkExposurePolicy(o);
|
|
267
|
+
if (!i.allowed)
|
|
268
|
+
return { success: !1, message: i.message };
|
|
269
|
+
const n = [];
|
|
230
270
|
try {
|
|
231
|
-
const
|
|
232
|
-
[
|
|
271
|
+
const l = await this.resolver.resolveToolsForToolsets(
|
|
272
|
+
[o],
|
|
233
273
|
this.context
|
|
234
274
|
);
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
};
|
|
240
|
-
if (this.exposurePolicy?.denylist && this.exposurePolicy.denylist.includes(t))
|
|
241
|
-
return {
|
|
242
|
-
success: !1,
|
|
243
|
-
message: `Toolset '${t}' is denied by policy.`
|
|
244
|
-
};
|
|
245
|
-
if (this.exposurePolicy?.maxActiveToolsets !== void 0 && this.activeToolsets.size + 1 > this.exposurePolicy.maxActiveToolsets)
|
|
246
|
-
return this.exposurePolicy.onLimitExceeded?.(
|
|
247
|
-
[t],
|
|
248
|
-
Array.from(this.activeToolsets)
|
|
249
|
-
), {
|
|
250
|
-
success: !1,
|
|
251
|
-
message: `Activation exceeds maxActiveToolsets (${this.exposurePolicy.maxActiveToolsets}).`
|
|
252
|
-
};
|
|
253
|
-
if (o && o.length > 0) {
|
|
254
|
-
const r = this.toolRegistry.mapAndValidate(
|
|
255
|
-
t,
|
|
256
|
-
o
|
|
275
|
+
if (l && l.length > 0) {
|
|
276
|
+
const a = this.toolRegistry.mapAndValidate(
|
|
277
|
+
o,
|
|
278
|
+
l
|
|
257
279
|
);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
this.activeToolsets.add(t);
|
|
261
|
-
try {
|
|
262
|
-
await this.onToolsListChanged?.();
|
|
263
|
-
} catch (r) {
|
|
264
|
-
console.warn("Failed to send tool list change notification:", r);
|
|
280
|
+
for (const c of a)
|
|
281
|
+
this.registerSingleTool(c, o), n.push(c.name);
|
|
265
282
|
}
|
|
266
|
-
return {
|
|
283
|
+
return this.activeToolsets.add(o), s || await this.notifyToolsChanged(), {
|
|
267
284
|
success: !0,
|
|
268
|
-
message: `Toolset '${
|
|
285
|
+
message: `Toolset '${o}' enabled successfully. Registered ${l?.length ?? 0} tools.`
|
|
269
286
|
};
|
|
270
|
-
} catch (
|
|
271
|
-
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
|
+
), {
|
|
272
291
|
success: !1,
|
|
273
|
-
message: `Failed to enable toolset '${
|
|
292
|
+
message: `Failed to enable toolset '${o}': ${l instanceof Error ? l.message : "Unknown error"}`
|
|
274
293
|
};
|
|
275
294
|
}
|
|
276
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
|
+
*/
|
|
277
337
|
async disableToolset(e) {
|
|
278
338
|
const s = this.resolver.validateToolsetName(e);
|
|
279
339
|
if (!s.isValid || !s.sanitized) {
|
|
@@ -284,20 +344,12 @@ class C {
|
|
|
284
344
|
};
|
|
285
345
|
}
|
|
286
346
|
const t = s.sanitized;
|
|
287
|
-
|
|
288
|
-
return {
|
|
289
|
-
success: !1,
|
|
290
|
-
message: `Toolset '${t}' is not currently active. Active toolsets: ${Array.from(this.activeToolsets).join(", ") || "none"}`
|
|
291
|
-
};
|
|
292
|
-
this.activeToolsets.delete(t);
|
|
293
|
-
try {
|
|
294
|
-
await this.onToolsListChanged?.();
|
|
295
|
-
} catch (o) {
|
|
296
|
-
console.warn("Failed to send tool list change notification:", o);
|
|
297
|
-
}
|
|
298
|
-
return {
|
|
347
|
+
return this.activeToolsets.has(t) ? (this.activeToolsets.delete(t), await this.notifyToolsChanged(), {
|
|
299
348
|
success: !0,
|
|
300
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"}`
|
|
301
353
|
};
|
|
302
354
|
}
|
|
303
355
|
getStatus() {
|
|
@@ -311,159 +363,181 @@ class C {
|
|
|
311
363
|
toolsetToTools: this.toolRegistry.listByToolset()
|
|
312
364
|
};
|
|
313
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
|
+
*/
|
|
314
372
|
async enableToolsets(e) {
|
|
315
373
|
const s = [];
|
|
316
|
-
for (const
|
|
374
|
+
for (const n of e)
|
|
317
375
|
try {
|
|
318
|
-
const
|
|
319
|
-
s.push({ name:
|
|
320
|
-
} catch (
|
|
376
|
+
const l = await this.enableToolset(n, !0);
|
|
377
|
+
s.push({ name: n, ...l });
|
|
378
|
+
} catch (l) {
|
|
321
379
|
s.push({
|
|
322
|
-
name:
|
|
380
|
+
name: n,
|
|
323
381
|
success: !1,
|
|
324
|
-
message:
|
|
382
|
+
message: l instanceof Error ? l.message : "Unknown error",
|
|
325
383
|
code: "E_INTERNAL"
|
|
326
384
|
});
|
|
327
385
|
}
|
|
328
|
-
const t = s.every((
|
|
329
|
-
|
|
330
|
-
try {
|
|
331
|
-
await this.onToolsListChanged?.();
|
|
332
|
-
} catch {
|
|
333
|
-
}
|
|
334
|
-
return { success: t, results: s, message: o };
|
|
335
|
-
}
|
|
336
|
-
registerDirectTools(e, s) {
|
|
337
|
-
for (const t of e)
|
|
338
|
-
try {
|
|
339
|
-
this.server.tool(
|
|
340
|
-
t.name,
|
|
341
|
-
t.description,
|
|
342
|
-
t.inputSchema,
|
|
343
|
-
async (o) => await t.handler(o)
|
|
344
|
-
), s ? this.toolRegistry.addForToolset(s, t.name) : this.toolRegistry.add(t.name);
|
|
345
|
-
} catch (o) {
|
|
346
|
-
throw console.error(`Failed to register direct tool '${t.name}':`, o), o;
|
|
347
|
-
}
|
|
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 };
|
|
348
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* Enables all available toolsets in a batch operation.
|
|
391
|
+
* @returns Result object with overall success status and individual results
|
|
392
|
+
*/
|
|
349
393
|
async enableAllToolsets() {
|
|
350
394
|
const e = this.getAvailableToolsets();
|
|
351
395
|
return this.enableToolsets(e);
|
|
352
396
|
}
|
|
353
397
|
}
|
|
354
|
-
function
|
|
355
|
-
|
|
356
|
-
a.tool(
|
|
398
|
+
function K(r, e, s) {
|
|
399
|
+
(s?.mode ?? "DYNAMIC") === "DYNAMIC" && (r.tool(
|
|
357
400
|
"enable_toolset",
|
|
358
401
|
"Enable a toolset by name",
|
|
359
|
-
{ name:
|
|
402
|
+
{ name: w.string().describe("Toolset name") },
|
|
360
403
|
async (o) => {
|
|
361
|
-
const
|
|
404
|
+
const i = await e.enableToolset(o.name);
|
|
362
405
|
return {
|
|
363
406
|
content: [{ type: "text", text: JSON.stringify(i) }]
|
|
364
407
|
};
|
|
365
408
|
}
|
|
366
|
-
),
|
|
409
|
+
), r.tool(
|
|
367
410
|
"disable_toolset",
|
|
368
411
|
"Disable a toolset by name (state only)",
|
|
369
|
-
{ name:
|
|
412
|
+
{ name: w.string().describe("Toolset name") },
|
|
370
413
|
async (o) => {
|
|
371
|
-
const
|
|
414
|
+
const i = await e.disableToolset(o.name);
|
|
372
415
|
return {
|
|
373
416
|
content: [{ type: "text", text: JSON.stringify(i) }]
|
|
374
417
|
};
|
|
375
418
|
}
|
|
376
|
-
),
|
|
419
|
+
), r.tool(
|
|
377
420
|
"list_toolsets",
|
|
378
421
|
"List available toolsets with active status and definitions",
|
|
379
422
|
{},
|
|
380
423
|
async () => {
|
|
381
|
-
const o = e.getAvailableToolsets(),
|
|
382
|
-
const
|
|
424
|
+
const o = e.getAvailableToolsets(), i = e.getStatus().toolsetToTools, n = o.map((l) => {
|
|
425
|
+
const a = e.getToolsetDefinition(l);
|
|
383
426
|
return {
|
|
384
427
|
key: l,
|
|
385
428
|
active: e.isActive(l),
|
|
386
|
-
definition:
|
|
387
|
-
name:
|
|
388
|
-
description:
|
|
389
|
-
modules:
|
|
390
|
-
decisionCriteria:
|
|
429
|
+
definition: a ? {
|
|
430
|
+
name: a.name,
|
|
431
|
+
description: a.description,
|
|
432
|
+
modules: a.modules ?? [],
|
|
433
|
+
decisionCriteria: a.decisionCriteria ?? void 0
|
|
391
434
|
} : null,
|
|
392
|
-
tools:
|
|
435
|
+
tools: i[l] ?? []
|
|
393
436
|
};
|
|
394
437
|
});
|
|
395
438
|
return {
|
|
396
439
|
content: [
|
|
397
|
-
{ type: "text", text: JSON.stringify({ toolsets:
|
|
440
|
+
{ type: "text", text: JSON.stringify({ toolsets: n }) }
|
|
398
441
|
]
|
|
399
442
|
};
|
|
400
443
|
}
|
|
401
|
-
),
|
|
444
|
+
), r.tool(
|
|
402
445
|
"describe_toolset",
|
|
403
446
|
"Describe a toolset with definition, active status and tools",
|
|
404
|
-
{ name:
|
|
447
|
+
{ name: w.string().describe("Toolset name") },
|
|
405
448
|
async (o) => {
|
|
406
|
-
const
|
|
449
|
+
const i = e.getToolsetDefinition(o.name), n = e.getStatus().toolsetToTools;
|
|
407
450
|
if (!i)
|
|
408
451
|
return {
|
|
409
452
|
content: [
|
|
410
453
|
{
|
|
411
454
|
type: "text",
|
|
412
|
-
text: JSON.stringify({ error: `Unknown toolset '${
|
|
455
|
+
text: JSON.stringify({ error: `Unknown toolset '${o.name}'` })
|
|
413
456
|
}
|
|
414
457
|
]
|
|
415
458
|
};
|
|
416
|
-
const
|
|
417
|
-
key:
|
|
418
|
-
active: e.isActive(
|
|
459
|
+
const l = {
|
|
460
|
+
key: o.name,
|
|
461
|
+
active: e.isActive(o.name),
|
|
419
462
|
definition: {
|
|
420
463
|
name: i.name,
|
|
421
464
|
description: i.description,
|
|
422
465
|
modules: i.modules ?? [],
|
|
423
466
|
decisionCriteria: i.decisionCriteria ?? void 0
|
|
424
467
|
},
|
|
425
|
-
tools:
|
|
468
|
+
tools: n[o.name] ?? []
|
|
426
469
|
};
|
|
427
470
|
return {
|
|
428
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
471
|
+
content: [{ type: "text", text: JSON.stringify(l) }]
|
|
429
472
|
};
|
|
430
473
|
}
|
|
431
|
-
)),
|
|
474
|
+
)), r.tool(
|
|
432
475
|
"list_tools",
|
|
433
476
|
"List currently registered tool names (best effort)",
|
|
434
477
|
{},
|
|
435
478
|
async () => {
|
|
436
|
-
const o = e.getStatus(),
|
|
479
|
+
const o = e.getStatus(), i = {
|
|
437
480
|
tools: o.tools,
|
|
438
481
|
toolsetToTools: o.toolsetToTools
|
|
439
482
|
};
|
|
440
483
|
return {
|
|
441
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
484
|
+
content: [{ type: "text", text: JSON.stringify(i) }]
|
|
442
485
|
};
|
|
443
486
|
}
|
|
444
487
|
);
|
|
445
488
|
}
|
|
446
|
-
class
|
|
489
|
+
class T {
|
|
447
490
|
constructor(e) {
|
|
448
|
-
this.toolsetValidator = new
|
|
491
|
+
this.initError = null, this.toolsetValidator = new q();
|
|
449
492
|
const s = e.startup ?? {}, t = this.resolveStartupConfig(s, e.catalog);
|
|
450
|
-
this.mode = t.mode, this.resolver = new
|
|
493
|
+
this.mode = t.mode, this.resolver = new J({
|
|
451
494
|
catalog: e.catalog,
|
|
452
495
|
moduleLoaders: e.moduleLoaders
|
|
453
496
|
});
|
|
454
|
-
const o = new
|
|
497
|
+
const o = new P({
|
|
455
498
|
namespaceWithToolset: e.exposurePolicy?.namespaceToolsWithSetKey ?? !0
|
|
456
499
|
});
|
|
457
|
-
this.manager = new
|
|
500
|
+
this.manager = new G({
|
|
458
501
|
server: e.server,
|
|
459
502
|
resolver: this.resolver,
|
|
460
503
|
context: e.context,
|
|
461
504
|
onToolsListChanged: e.notifyToolsListChanged,
|
|
462
505
|
exposurePolicy: e.exposurePolicy,
|
|
463
506
|
toolRegistry: o
|
|
464
|
-
}), e.registerMetaTools !== !1 &&
|
|
465
|
-
const
|
|
466
|
-
|
|
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;
|
|
533
|
+
}
|
|
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;
|
|
467
541
|
}
|
|
468
542
|
resolveStartupConfig(e, s) {
|
|
469
543
|
if (e.mode) {
|
|
@@ -473,9 +547,9 @@ class y {
|
|
|
473
547
|
if (e.toolsets === "ALL")
|
|
474
548
|
return { mode: "STATIC", toolsets: "ALL" };
|
|
475
549
|
const t = Array.isArray(e.toolsets) ? e.toolsets : [], o = [];
|
|
476
|
-
for (const
|
|
477
|
-
const { isValid:
|
|
478
|
-
|
|
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);
|
|
479
553
|
}
|
|
480
554
|
if (t.length > 0 && o.length === 0)
|
|
481
555
|
throw new Error(
|
|
@@ -489,8 +563,8 @@ class y {
|
|
|
489
563
|
if (Array.isArray(e.toolsets) && e.toolsets.length > 0) {
|
|
490
564
|
const t = [];
|
|
491
565
|
for (const o of e.toolsets) {
|
|
492
|
-
const { isValid:
|
|
493
|
-
|
|
566
|
+
const { isValid: i, sanitized: n, error: l } = this.toolsetValidator.validateToolsetName(o, s);
|
|
567
|
+
i && n ? t.push(n) : l && console.warn(l);
|
|
494
568
|
}
|
|
495
569
|
if (t.length === 0)
|
|
496
570
|
throw new Error(
|
|
@@ -507,9 +581,11 @@ class y {
|
|
|
507
581
|
return this.manager;
|
|
508
582
|
}
|
|
509
583
|
}
|
|
510
|
-
|
|
584
|
+
var p, b;
|
|
585
|
+
class $ {
|
|
511
586
|
constructor(e = {}) {
|
|
512
|
-
|
|
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;
|
|
513
589
|
const s = e.pruneIntervalMs ?? 1e3 * 60 * 10;
|
|
514
590
|
this.pruneInterval = setInterval(() => this.pruneExpired(), s);
|
|
515
591
|
}
|
|
@@ -531,25 +607,76 @@ class M {
|
|
|
531
607
|
const t = { resource: s, lastAccessed: Date.now() };
|
|
532
608
|
this.storage.set(e, t);
|
|
533
609
|
}
|
|
610
|
+
/**
|
|
611
|
+
* Removes an entry from the cache.
|
|
612
|
+
* Calls the onEvict callback if configured.
|
|
613
|
+
* @param key - The key to remove
|
|
614
|
+
*/
|
|
534
615
|
delete(e) {
|
|
535
|
-
this.storage.
|
|
616
|
+
const s = this.storage.get(e);
|
|
617
|
+
s && (this.storage.delete(e), u(this, p, b).call(this, e, s.resource));
|
|
536
618
|
}
|
|
537
|
-
|
|
538
|
-
|
|
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();
|
|
539
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);
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Evicts the least recently used entry from the cache.
|
|
638
|
+
* @private
|
|
639
|
+
*/
|
|
540
640
|
evictLeastRecentlyUsed() {
|
|
541
641
|
const e = this.storage.keys().next().value;
|
|
542
642
|
e && this.delete(e);
|
|
543
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* Removes all expired entries from the cache.
|
|
646
|
+
* @private
|
|
647
|
+
*/
|
|
544
648
|
pruneExpired() {
|
|
545
|
-
const e = Date.now();
|
|
546
|
-
for (const [
|
|
547
|
-
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);
|
|
548
654
|
}
|
|
549
655
|
}
|
|
550
|
-
|
|
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 {
|
|
551
674
|
constructor(e, s, t = {}, o) {
|
|
552
|
-
this.app = null, this.clientCache = new
|
|
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 = {
|
|
553
680
|
host: t.host ?? "0.0.0.0",
|
|
554
681
|
port: t.port ?? 3e3,
|
|
555
682
|
basePath: t.basePath ?? "/",
|
|
@@ -560,8 +687,8 @@ class L {
|
|
|
560
687
|
}
|
|
561
688
|
async start() {
|
|
562
689
|
if (this.app) return;
|
|
563
|
-
const e = this.options.app ??
|
|
564
|
-
this.options.cors && await e.register(
|
|
690
|
+
const e = this.options.app ?? M({ logger: this.options.logger });
|
|
691
|
+
this.options.cors && await e.register(x, { origin: !0 });
|
|
565
692
|
const s = this.options.basePath.endsWith("/") ? this.options.basePath.slice(0, -1) : this.options.basePath;
|
|
566
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 ?? {
|
|
567
694
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -575,30 +702,30 @@ class L {
|
|
|
575
702
|
})), e.post(
|
|
576
703
|
`${s}/mcp`,
|
|
577
704
|
async (t, o) => {
|
|
578
|
-
const
|
|
579
|
-
let
|
|
580
|
-
if (!
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
server:
|
|
584
|
-
orchestrator:
|
|
585
|
-
sessions:
|
|
586
|
-
}, l && this.clientCache.set(
|
|
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,
|
|
712
|
+
sessions: g instanceof Map ? g : /* @__PURE__ */ new Map()
|
|
713
|
+
}, l && this.clientCache.set(n, a);
|
|
587
714
|
}
|
|
588
715
|
const c = t.headers["mcp-session-id"];
|
|
589
716
|
let d;
|
|
590
|
-
if (c &&
|
|
591
|
-
d =
|
|
592
|
-
else if (!c &&
|
|
593
|
-
const
|
|
594
|
-
d = new
|
|
595
|
-
sessionIdGenerator: () =>
|
|
596
|
-
onsessioninitialized: (
|
|
597
|
-
|
|
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,
|
|
723
|
+
onsessioninitialized: (g) => {
|
|
724
|
+
a.sessions.set(g, d);
|
|
598
725
|
}
|
|
599
726
|
});
|
|
600
727
|
try {
|
|
601
|
-
await
|
|
728
|
+
await a.server.connect(d);
|
|
602
729
|
} catch {
|
|
603
730
|
return o.code(500), {
|
|
604
731
|
jsonrpc: "2.0",
|
|
@@ -607,7 +734,7 @@ class L {
|
|
|
607
734
|
};
|
|
608
735
|
}
|
|
609
736
|
d.onclose = () => {
|
|
610
|
-
d?.sessionId &&
|
|
737
|
+
d?.sessionId && a.sessions.delete(d.sessionId);
|
|
611
738
|
};
|
|
612
739
|
} else
|
|
613
740
|
return o.code(400), {
|
|
@@ -622,22 +749,22 @@ class L {
|
|
|
622
749
|
), o;
|
|
623
750
|
}
|
|
624
751
|
), e.get(`${s}/mcp`, async (t, o) => {
|
|
625
|
-
const
|
|
626
|
-
if (!
|
|
752
|
+
const i = t.headers["mcp-client-id"]?.trim(), n = i && i.length > 0 ? i : "";
|
|
753
|
+
if (!n)
|
|
627
754
|
return o.code(400), "Missing mcp-client-id";
|
|
628
|
-
const l = this.clientCache.get(
|
|
755
|
+
const l = this.clientCache.get(n);
|
|
629
756
|
if (!l)
|
|
630
757
|
return o.code(400), "Invalid or expired client";
|
|
631
|
-
const
|
|
632
|
-
if (!
|
|
758
|
+
const a = t.headers["mcp-session-id"];
|
|
759
|
+
if (!a)
|
|
633
760
|
return o.code(400), "Missing mcp-session-id";
|
|
634
|
-
const c = l.sessions.get(
|
|
761
|
+
const c = l.sessions.get(a);
|
|
635
762
|
return c ? (await c.handleRequest(t.raw, o.raw), o) : (o.code(400), "Invalid or expired session ID");
|
|
636
763
|
}), e.delete(
|
|
637
764
|
`${s}/mcp`,
|
|
638
765
|
async (t, o) => {
|
|
639
|
-
const
|
|
640
|
-
if (!
|
|
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)
|
|
641
768
|
return o.code(400), {
|
|
642
769
|
jsonrpc: "2.0",
|
|
643
770
|
error: {
|
|
@@ -646,8 +773,8 @@ class L {
|
|
|
646
773
|
},
|
|
647
774
|
id: null
|
|
648
775
|
};
|
|
649
|
-
const
|
|
650
|
-
if (!
|
|
776
|
+
const a = this.clientCache.get(n), c = a?.sessions.get(l);
|
|
777
|
+
if (!a || !c)
|
|
651
778
|
return o.code(404), {
|
|
652
779
|
jsonrpc: "2.0",
|
|
653
780
|
error: { code: -32e3, message: "Session not found or expired" },
|
|
@@ -660,59 +787,81 @@ class L {
|
|
|
660
787
|
} catch {
|
|
661
788
|
}
|
|
662
789
|
} finally {
|
|
663
|
-
c?.sessionId ?
|
|
790
|
+
c?.sessionId ? a.sessions.delete(c.sessionId) : a.sessions.delete(l);
|
|
664
791
|
}
|
|
665
792
|
return o.code(204).send(), o;
|
|
666
793
|
}
|
|
667
794
|
), this.options.app || await e.listen({ host: this.options.host, port: this.options.port }), this.app = e;
|
|
668
795
|
}
|
|
796
|
+
/**
|
|
797
|
+
* Stops the Fastify server and cleans up all resources.
|
|
798
|
+
* Closes all client sessions and clears the cache.
|
|
799
|
+
*/
|
|
669
800
|
async stop() {
|
|
670
|
-
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();
|
|
671
819
|
}
|
|
672
820
|
}
|
|
673
|
-
async function
|
|
674
|
-
const e =
|
|
675
|
-
if (typeof
|
|
821
|
+
async function ge(r) {
|
|
822
|
+
const e = r.startup?.mode ?? "DYNAMIC";
|
|
823
|
+
if (typeof r.createServer != "function")
|
|
676
824
|
throw new Error("createMcpServer: `createServer` (factory) is required");
|
|
677
|
-
const s =
|
|
825
|
+
const s = r.createServer(), t = (a) => typeof a?.server?.notification == "function", o = (a) => typeof a?.notifyToolsListChanged == "function", i = async (a) => {
|
|
678
826
|
try {
|
|
679
|
-
if (t(
|
|
680
|
-
await
|
|
827
|
+
if (t(a)) {
|
|
828
|
+
await a.server.notification({
|
|
681
829
|
method: "notifications/tools/list_changed"
|
|
682
830
|
});
|
|
683
831
|
return;
|
|
684
832
|
}
|
|
685
|
-
o(
|
|
686
|
-
} catch {
|
|
833
|
+
o(a) && await a.notifyToolsListChanged();
|
|
834
|
+
} catch (c) {
|
|
835
|
+
console.warn("Failed to send tools list changed notification:", c);
|
|
687
836
|
}
|
|
688
|
-
},
|
|
837
|
+
}, n = new T({
|
|
689
838
|
server: s,
|
|
690
|
-
catalog:
|
|
691
|
-
moduleLoaders:
|
|
692
|
-
exposurePolicy:
|
|
693
|
-
context:
|
|
694
|
-
notifyToolsListChanged: async () =>
|
|
695
|
-
startup:
|
|
696
|
-
registerMetaTools:
|
|
697
|
-
}), l = new
|
|
698
|
-
|
|
839
|
+
catalog: r.catalog,
|
|
840
|
+
moduleLoaders: r.moduleLoaders,
|
|
841
|
+
exposurePolicy: r.exposurePolicy,
|
|
842
|
+
context: r.context,
|
|
843
|
+
notifyToolsListChanged: async () => i(s),
|
|
844
|
+
startup: r.startup,
|
|
845
|
+
registerMetaTools: r.registerMetaTools !== void 0 ? r.registerMetaTools : e === "DYNAMIC"
|
|
846
|
+
}), l = new Q(
|
|
847
|
+
n.getManager(),
|
|
699
848
|
() => {
|
|
700
849
|
if (e === "STATIC")
|
|
701
|
-
return { server: s, orchestrator:
|
|
702
|
-
const
|
|
703
|
-
server:
|
|
704
|
-
catalog:
|
|
705
|
-
moduleLoaders:
|
|
706
|
-
exposurePolicy:
|
|
707
|
-
context:
|
|
708
|
-
notifyToolsListChanged: async () =>
|
|
709
|
-
startup:
|
|
710
|
-
registerMetaTools:
|
|
850
|
+
return { server: s, orchestrator: n };
|
|
851
|
+
const a = r.createServer(), c = new T({
|
|
852
|
+
server: a,
|
|
853
|
+
catalog: r.catalog,
|
|
854
|
+
moduleLoaders: r.moduleLoaders,
|
|
855
|
+
exposurePolicy: r.exposurePolicy,
|
|
856
|
+
context: r.context,
|
|
857
|
+
notifyToolsListChanged: async () => i(a),
|
|
858
|
+
startup: r.startup,
|
|
859
|
+
registerMetaTools: r.registerMetaTools !== void 0 ? r.registerMetaTools : e === "DYNAMIC"
|
|
711
860
|
});
|
|
712
|
-
return { server:
|
|
861
|
+
return { server: a, orchestrator: c };
|
|
713
862
|
},
|
|
714
|
-
|
|
715
|
-
|
|
863
|
+
r.http,
|
|
864
|
+
r.configSchema
|
|
716
865
|
);
|
|
717
866
|
return {
|
|
718
867
|
server: s,
|
|
@@ -724,7 +873,561 @@ async function O(a) {
|
|
|
724
873
|
}
|
|
725
874
|
};
|
|
726
875
|
}
|
|
876
|
+
function X(r) {
|
|
877
|
+
Z(r), ee(r), se(r), te(r);
|
|
878
|
+
}
|
|
879
|
+
function Z(r) {
|
|
880
|
+
if (!r || typeof r != "object")
|
|
881
|
+
throw new Error(
|
|
882
|
+
"Permission configuration is required for createPermissionBasedMcpServer"
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
function ee(r) {
|
|
886
|
+
if (!r.source)
|
|
887
|
+
throw new Error('Permission source must be either "headers" or "config"');
|
|
888
|
+
if (r.source !== "headers" && r.source !== "config")
|
|
889
|
+
throw new Error(
|
|
890
|
+
`Invalid permission source: "${r.source}". Must be either "headers" or "config"`
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
function se(r) {
|
|
894
|
+
if (r.source === "config" && !r.staticMap && !r.resolver)
|
|
895
|
+
throw new Error(
|
|
896
|
+
"Config-based permissions require at least one of: staticMap or resolver function"
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
function te(r) {
|
|
900
|
+
if (r.staticMap !== void 0) {
|
|
901
|
+
if (typeof r.staticMap != "object" || r.staticMap === null)
|
|
902
|
+
throw new Error(
|
|
903
|
+
"staticMap must be an object mapping client IDs to toolset arrays"
|
|
904
|
+
);
|
|
905
|
+
oe(r.staticMap);
|
|
906
|
+
}
|
|
907
|
+
if (r.resolver !== void 0 && typeof r.resolver != "function")
|
|
908
|
+
throw new Error(
|
|
909
|
+
"resolver must be a synchronous function: (clientId: string) => string[]"
|
|
910
|
+
);
|
|
911
|
+
if (r.defaultPermissions !== void 0 && !Array.isArray(r.defaultPermissions))
|
|
912
|
+
throw new Error("defaultPermissions must be an array of toolset names");
|
|
913
|
+
if (r.headerName !== void 0 && (typeof r.headerName != "string" || r.headerName.length === 0))
|
|
914
|
+
throw new Error("headerName must be a non-empty string");
|
|
915
|
+
}
|
|
916
|
+
function oe(r) {
|
|
917
|
+
for (const [e, s] of Object.entries(r))
|
|
918
|
+
if (!Array.isArray(s))
|
|
919
|
+
throw new Error(
|
|
920
|
+
`staticMap value for client "${e}" must be an array of toolset names`
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
var f, L, j, N, k, D;
|
|
924
|
+
class re {
|
|
925
|
+
/**
|
|
926
|
+
* Creates a new PermissionResolver instance.
|
|
927
|
+
* @param config - The permission configuration defining how permissions are resolved
|
|
928
|
+
*/
|
|
929
|
+
constructor(e) {
|
|
930
|
+
y(this, f);
|
|
931
|
+
this.config = e, this.cache = /* @__PURE__ */ new Map(), this.normalizedHeaderName = (e.headerName || "mcp-toolset-permissions").toLowerCase();
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Resolves permissions for a client based on the configured source.
|
|
935
|
+
* Results are cached to improve performance for subsequent requests from the same client.
|
|
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
|
+
*
|
|
942
|
+
* @param clientId - The unique identifier for the client
|
|
943
|
+
* @param headers - Optional request headers (required for header-based permissions)
|
|
944
|
+
* @returns Array of toolset names the client is allowed to access
|
|
945
|
+
*/
|
|
946
|
+
resolvePermissions(e, s) {
|
|
947
|
+
if (this.cache.has(e))
|
|
948
|
+
return this.cache.get(e);
|
|
949
|
+
let t;
|
|
950
|
+
try {
|
|
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(
|
|
952
|
+
`Permission resolution returned non-array for client ${e}, using empty permissions`
|
|
953
|
+
), t = []), t = t.filter(
|
|
954
|
+
(o) => typeof o == "string" && o.trim().length > 0
|
|
955
|
+
);
|
|
956
|
+
} catch (o) {
|
|
957
|
+
console.error(
|
|
958
|
+
`Unexpected error resolving permissions for client ${e}:`,
|
|
959
|
+
o
|
|
960
|
+
), t = [];
|
|
961
|
+
}
|
|
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);
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Clears the permission cache.
|
|
974
|
+
* Useful for cleanup during server shutdown or when permissions need to be refreshed.
|
|
975
|
+
*/
|
|
976
|
+
clearCache() {
|
|
977
|
+
this.cache.clear();
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
f = new WeakSet(), /**
|
|
981
|
+
* Parses permissions from request headers.
|
|
982
|
+
* Extracts comma-separated toolset names from the configured header.
|
|
983
|
+
* Handles malformed headers gracefully by returning empty permissions.
|
|
984
|
+
* Uses case-insensitive header lookup per RFC 7230.
|
|
985
|
+
* @param headers - Request headers containing permission data
|
|
986
|
+
* @returns Array of toolset names from headers, or empty array if header is missing/malformed
|
|
987
|
+
* @private
|
|
988
|
+
*/
|
|
989
|
+
L = function(e) {
|
|
990
|
+
if (!e)
|
|
991
|
+
return [];
|
|
992
|
+
const s = u(this, f, j).call(this, e, this.normalizedHeaderName);
|
|
993
|
+
if (!s)
|
|
994
|
+
return [];
|
|
995
|
+
try {
|
|
996
|
+
return s.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
997
|
+
} catch (t) {
|
|
998
|
+
return console.warn(
|
|
999
|
+
`Failed to parse permission header '${this.normalizedHeaderName}':`,
|
|
1000
|
+
t
|
|
1001
|
+
), [];
|
|
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;
|
|
1017
|
+
}, /**
|
|
1018
|
+
* Resolves permissions from server-side configuration.
|
|
1019
|
+
* Tries resolver function first (if provided), then falls back to static map,
|
|
1020
|
+
* and finally to default permissions. Handles errors gracefully.
|
|
1021
|
+
* @param clientId - The unique identifier for the client
|
|
1022
|
+
* @returns Array of toolset names from configuration
|
|
1023
|
+
* @private
|
|
1024
|
+
*/
|
|
1025
|
+
N = function(e) {
|
|
1026
|
+
if (this.config.resolver) {
|
|
1027
|
+
const s = u(this, f, k).call(this, e);
|
|
1028
|
+
if (s !== null)
|
|
1029
|
+
return s;
|
|
1030
|
+
}
|
|
1031
|
+
if (this.config.staticMap) {
|
|
1032
|
+
const s = u(this, f, D).call(this, e);
|
|
1033
|
+
if (s !== null)
|
|
1034
|
+
return s;
|
|
1035
|
+
}
|
|
1036
|
+
return this.config.defaultPermissions || [];
|
|
1037
|
+
}, /**
|
|
1038
|
+
* Attempts to resolve permissions using the configured resolver function.
|
|
1039
|
+
* Handles errors gracefully and returns null on failure to allow fallback.
|
|
1040
|
+
* @param clientId - The unique identifier for the client
|
|
1041
|
+
* @returns Array of toolset names if successful, null if resolver fails or returns invalid data
|
|
1042
|
+
* @private
|
|
1043
|
+
*/
|
|
1044
|
+
k = function(e) {
|
|
1045
|
+
try {
|
|
1046
|
+
const s = this.config.resolver(e);
|
|
1047
|
+
return Array.isArray(s) ? s : (console.warn(
|
|
1048
|
+
`Permission resolver returned non-array for client ${e}, using fallback`
|
|
1049
|
+
), null);
|
|
1050
|
+
} catch (s) {
|
|
1051
|
+
const t = s instanceof Error ? s.message : String(s);
|
|
1052
|
+
return console.warn(
|
|
1053
|
+
`Permission resolver declined client ${e} (${t}), trying fallback`
|
|
1054
|
+
), null;
|
|
1055
|
+
}
|
|
1056
|
+
}, /**
|
|
1057
|
+
* Looks up permissions in the static map configuration.
|
|
1058
|
+
* Returns null if client is not found to allow fallback to defaults.
|
|
1059
|
+
* @param clientId - The unique identifier for the client
|
|
1060
|
+
* @returns Array of toolset names if found, null if client not in map
|
|
1061
|
+
* @private
|
|
1062
|
+
*/
|
|
1063
|
+
D = function(e) {
|
|
1064
|
+
const s = this.config.staticMap[e];
|
|
1065
|
+
return s !== void 0 ? Array.isArray(s) ? s : [] : null;
|
|
1066
|
+
};
|
|
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 {
|
|
1085
|
+
server: o.server,
|
|
1086
|
+
orchestrator: o.orchestrator,
|
|
1087
|
+
allowedToolsets: n,
|
|
1088
|
+
failedToolsets: l
|
|
1089
|
+
};
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
var h, z, R, O, F, V, _, B, Y, H, U;
|
|
1093
|
+
class ne {
|
|
1094
|
+
/**
|
|
1095
|
+
* Creates a new PermissionAwareFastifyTransport instance.
|
|
1096
|
+
* @param defaultManager - Default tool manager for status endpoints
|
|
1097
|
+
* @param createPermissionAwareBundle - Function to create permission-aware bundles
|
|
1098
|
+
* @param options - Transport configuration options
|
|
1099
|
+
* @param configSchema - Optional JSON schema for configuration discovery
|
|
1100
|
+
*/
|
|
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
|
|
1114
|
+
}, this.configSchema = o;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Starts the Fastify server and registers all MCP endpoints.
|
|
1118
|
+
* Sets up routes for health checks, tool status, and MCP protocol handling.
|
|
1119
|
+
*/
|
|
1120
|
+
async start() {
|
|
1121
|
+
if (this.app) return;
|
|
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;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Stops the Fastify server and cleans up all resources.
|
|
1129
|
+
* Closes all client sessions and clears the cache.
|
|
1130
|
+
*/
|
|
1131
|
+
async stop() {
|
|
1132
|
+
this.app && (this.clientCache.stop(!0), this.options.app || await this.app.close(), this.app = null);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
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
|
+
}, /**
|
|
1152
|
+
* Normalizes the base path by removing trailing slashes.
|
|
1153
|
+
* @param basePath - The base path to normalize
|
|
1154
|
+
* @returns Normalized base path without trailing slash
|
|
1155
|
+
* @private
|
|
1156
|
+
*/
|
|
1157
|
+
R = function(e) {
|
|
1158
|
+
return e.endsWith("/") ? e.slice(0, -1) : e;
|
|
1159
|
+
}, /**
|
|
1160
|
+
* Registers the health check endpoint.
|
|
1161
|
+
* @param app - Fastify instance
|
|
1162
|
+
* @param base - Base path for routes
|
|
1163
|
+
* @private
|
|
1164
|
+
*/
|
|
1165
|
+
O = function(e, s) {
|
|
1166
|
+
e.get(`${s}/healthz`, async () => ({ ok: !0 }));
|
|
1167
|
+
}, /**
|
|
1168
|
+
* Registers the tools status endpoint.
|
|
1169
|
+
* @param app - Fastify instance
|
|
1170
|
+
* @param base - Base path for routes
|
|
1171
|
+
* @private
|
|
1172
|
+
*/
|
|
1173
|
+
F = function(e, s) {
|
|
1174
|
+
e.get(`${s}/tools`, async () => this.defaultManager.getStatus());
|
|
1175
|
+
}, /**
|
|
1176
|
+
* Registers the MCP configuration discovery endpoint.
|
|
1177
|
+
* @param app - Fastify instance
|
|
1178
|
+
* @param base - Base path for routes
|
|
1179
|
+
* @private
|
|
1180
|
+
*/
|
|
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 ?? {
|
|
1183
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
1184
|
+
title: "MCP Session Configuration",
|
|
1185
|
+
description: "Schema for the /mcp endpoint configuration",
|
|
1186
|
+
type: "object",
|
|
1187
|
+
properties: {},
|
|
1188
|
+
required: [],
|
|
1189
|
+
"x-mcp-version": "1.0",
|
|
1190
|
+
"x-query-style": "dot+bracket"
|
|
1191
|
+
}));
|
|
1192
|
+
}, /**
|
|
1193
|
+
* Registers the POST /mcp endpoint for JSON-RPC requests.
|
|
1194
|
+
* Extracts client context, resolves permissions, and handles MCP protocol.
|
|
1195
|
+
* @param app - Fastify instance
|
|
1196
|
+
* @param base - Base path for routes
|
|
1197
|
+
* @private
|
|
1198
|
+
*/
|
|
1199
|
+
_ = function(e, s) {
|
|
1200
|
+
e.post(
|
|
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;
|
|
1205
|
+
if (!l)
|
|
1206
|
+
try {
|
|
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;
|
|
1212
|
+
l = {
|
|
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) {
|
|
1220
|
+
return console.error(
|
|
1221
|
+
`Failed to create permission-aware bundle for client ${i.clientId}:`,
|
|
1222
|
+
d
|
|
1223
|
+
), o.code(403), u(this, h, U).call(this, "Access denied");
|
|
1224
|
+
}
|
|
1225
|
+
const a = t.headers["mcp-session-id"];
|
|
1226
|
+
let c;
|
|
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);
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
try {
|
|
1238
|
+
await l.server.connect(c);
|
|
1239
|
+
} catch {
|
|
1240
|
+
return o.code(500), {
|
|
1241
|
+
jsonrpc: "2.0",
|
|
1242
|
+
error: { code: -32603, message: "Error initializing server." },
|
|
1243
|
+
id: null
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
c.onclose = () => {
|
|
1247
|
+
c?.sessionId && l.sessions.delete(c.sessionId);
|
|
1248
|
+
};
|
|
1249
|
+
} else
|
|
1250
|
+
return o.code(400), {
|
|
1251
|
+
jsonrpc: "2.0",
|
|
1252
|
+
error: { code: -32e3, message: "Session not found or expired" },
|
|
1253
|
+
id: null
|
|
1254
|
+
};
|
|
1255
|
+
return await c.handleRequest(
|
|
1256
|
+
t.raw,
|
|
1257
|
+
o.raw,
|
|
1258
|
+
t.body
|
|
1259
|
+
), o;
|
|
1260
|
+
}
|
|
1261
|
+
);
|
|
1262
|
+
}, /**
|
|
1263
|
+
* Registers the GET /mcp endpoint for SSE notifications.
|
|
1264
|
+
* @param app - Fastify instance
|
|
1265
|
+
* @param base - Base path for routes
|
|
1266
|
+
* @private
|
|
1267
|
+
*/
|
|
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)
|
|
1272
|
+
return o.code(400), "Missing mcp-client-id";
|
|
1273
|
+
const l = this.clientCache.get(n);
|
|
1274
|
+
if (!l)
|
|
1275
|
+
return o.code(400), "Invalid or expired client";
|
|
1276
|
+
const a = t.headers["mcp-session-id"];
|
|
1277
|
+
if (!a)
|
|
1278
|
+
return o.code(400), "Missing mcp-session-id";
|
|
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");
|
|
1281
|
+
});
|
|
1282
|
+
}, /**
|
|
1283
|
+
* Registers the DELETE /mcp endpoint for session termination.
|
|
1284
|
+
* @param app - Fastify instance
|
|
1285
|
+
* @param base - Base path for routes
|
|
1286
|
+
* @private
|
|
1287
|
+
*/
|
|
1288
|
+
Y = function(e, s) {
|
|
1289
|
+
e.delete(
|
|
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)
|
|
1294
|
+
return o.code(400), {
|
|
1295
|
+
jsonrpc: "2.0",
|
|
1296
|
+
error: {
|
|
1297
|
+
code: -32600,
|
|
1298
|
+
message: "Missing mcp-client-id or mcp-session-id header"
|
|
1299
|
+
},
|
|
1300
|
+
id: null
|
|
1301
|
+
};
|
|
1302
|
+
const a = this.clientCache.get(n), c = a?.sessions.get(l);
|
|
1303
|
+
if (!a || !c)
|
|
1304
|
+
return o.code(404), {
|
|
1305
|
+
jsonrpc: "2.0",
|
|
1306
|
+
error: { code: -32e3, message: "Session not found or expired" },
|
|
1307
|
+
id: null
|
|
1308
|
+
};
|
|
1309
|
+
try {
|
|
1310
|
+
if (typeof c.close == "function")
|
|
1311
|
+
try {
|
|
1312
|
+
await c.close();
|
|
1313
|
+
} catch {
|
|
1314
|
+
}
|
|
1315
|
+
} finally {
|
|
1316
|
+
c?.sessionId ? a.sessions.delete(c.sessionId) : a.sessions.delete(l);
|
|
1317
|
+
}
|
|
1318
|
+
return o.code(204).send(), o;
|
|
1319
|
+
}
|
|
1320
|
+
);
|
|
1321
|
+
}, /**
|
|
1322
|
+
* Extracts client context from the request.
|
|
1323
|
+
* Generates anonymous client ID if not provided in headers.
|
|
1324
|
+
* @param req - Fastify request object
|
|
1325
|
+
* @returns Client request context with ID and headers
|
|
1326
|
+
* @private
|
|
1327
|
+
*/
|
|
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 };
|
|
1333
|
+
}, /**
|
|
1334
|
+
* Creates a safe error response that doesn't expose unauthorized toolset information.
|
|
1335
|
+
* Used for permission-related errors to prevent information leakage.
|
|
1336
|
+
* @param message - Generic error message to return to client
|
|
1337
|
+
* @param code - JSON-RPC error code (default: -32000 for server error)
|
|
1338
|
+
* @returns JSON-RPC error response object
|
|
1339
|
+
* @private
|
|
1340
|
+
*/
|
|
1341
|
+
U = function(e = "Access denied", s = -32e3) {
|
|
1342
|
+
return {
|
|
1343
|
+
jsonrpc: "2.0",
|
|
1344
|
+
error: {
|
|
1345
|
+
code: s,
|
|
1346
|
+
message: e
|
|
1347
|
+
},
|
|
1348
|
+
id: null
|
|
1349
|
+
};
|
|
1350
|
+
};
|
|
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) {
|
|
1367
|
+
if (!r.permissions)
|
|
1368
|
+
throw new Error(
|
|
1369
|
+
"Permission configuration is required for createPermissionBasedMcpServer. Please provide a 'permissions' field in the options."
|
|
1370
|
+
);
|
|
1371
|
+
if (X(r.permissions), r.startup)
|
|
1372
|
+
throw new Error(
|
|
1373
|
+
"Permission-based servers determine toolsets from client permissions. The 'startup' option is not allowed. Remove it from your configuration."
|
|
1374
|
+
);
|
|
1375
|
+
if (typeof r.createServer != "function")
|
|
1376
|
+
throw new Error(
|
|
1377
|
+
"createPermissionBasedMcpServer: `createServer` (factory) is required"
|
|
1378
|
+
);
|
|
1379
|
+
const e = ae(
|
|
1380
|
+
r.exposurePolicy
|
|
1381
|
+
), s = new re(r.permissions), t = r.createServer(), o = new T({
|
|
1382
|
+
server: t,
|
|
1383
|
+
catalog: r.catalog,
|
|
1384
|
+
moduleLoaders: r.moduleLoaders,
|
|
1385
|
+
exposurePolicy: e,
|
|
1386
|
+
context: r.context,
|
|
1387
|
+
notifyToolsListChanged: void 0,
|
|
1388
|
+
// No notifications in STATIC mode
|
|
1389
|
+
startup: { mode: "STATIC", toolsets: [] },
|
|
1390
|
+
registerMetaTools: !1
|
|
1391
|
+
}), i = ie(
|
|
1392
|
+
(l) => {
|
|
1393
|
+
const a = r.createServer(), c = new T({
|
|
1394
|
+
server: a,
|
|
1395
|
+
catalog: r.catalog,
|
|
1396
|
+
moduleLoaders: r.moduleLoaders,
|
|
1397
|
+
exposurePolicy: e,
|
|
1398
|
+
context: r.context,
|
|
1399
|
+
notifyToolsListChanged: void 0,
|
|
1400
|
+
// No notifications in STATIC mode
|
|
1401
|
+
startup: { mode: "STATIC", toolsets: [] },
|
|
1402
|
+
// Empty - we'll enable manually
|
|
1403
|
+
registerMetaTools: !1
|
|
1404
|
+
// No meta-tools - toolsets are fixed per client
|
|
1405
|
+
});
|
|
1406
|
+
return { server: a, orchestrator: c };
|
|
1407
|
+
},
|
|
1408
|
+
s
|
|
1409
|
+
), n = new ne(
|
|
1410
|
+
o.getManager(),
|
|
1411
|
+
i,
|
|
1412
|
+
r.http,
|
|
1413
|
+
r.configSchema
|
|
1414
|
+
);
|
|
1415
|
+
return {
|
|
1416
|
+
server: t,
|
|
1417
|
+
start: async () => {
|
|
1418
|
+
await n.start();
|
|
1419
|
+
},
|
|
1420
|
+
close: async () => {
|
|
1421
|
+
try {
|
|
1422
|
+
await n.stop();
|
|
1423
|
+
} finally {
|
|
1424
|
+
s.clearCache();
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
727
1429
|
export {
|
|
728
|
-
|
|
1430
|
+
ge as createMcpServer,
|
|
1431
|
+
pe as createPermissionBasedMcpServer
|
|
729
1432
|
};
|
|
730
1433
|
//# sourceMappingURL=index.js.map
|