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.
Files changed (32) hide show
  1. package/README.md +945 -21
  2. package/dist/core/DynamicToolManager.d.ts +45 -2
  3. package/dist/core/DynamicToolManager.d.ts.map +1 -1
  4. package/dist/core/ServerOrchestrator.d.ts +24 -2
  5. package/dist/core/ServerOrchestrator.d.ts.map +1 -1
  6. package/dist/http/FastifyTransport.d.ts +11 -0
  7. package/dist/http/FastifyTransport.d.ts.map +1 -1
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +940 -237
  11. package/dist/index.js.map +1 -1
  12. package/dist/meta/registerMetaTools.d.ts +14 -0
  13. package/dist/meta/registerMetaTools.d.ts.map +1 -1
  14. package/dist/mode/ModeResolver.d.ts +7 -0
  15. package/dist/mode/ModeResolver.d.ts.map +1 -1
  16. package/dist/permissions/PermissionAwareFastifyTransport.d.ts +47 -0
  17. package/dist/permissions/PermissionAwareFastifyTransport.d.ts.map +1 -0
  18. package/dist/permissions/PermissionResolver.d.ts +43 -0
  19. package/dist/permissions/PermissionResolver.d.ts.map +1 -0
  20. package/dist/permissions/createPermissionAwareBundle.d.ts +58 -0
  21. package/dist/permissions/createPermissionAwareBundle.d.ts.map +1 -0
  22. package/dist/permissions/validatePermissionConfig.d.ts +9 -0
  23. package/dist/permissions/validatePermissionConfig.d.ts.map +1 -0
  24. package/dist/server/createMcpServer.d.ts +4 -3
  25. package/dist/server/createMcpServer.d.ts.map +1 -1
  26. package/dist/server/createPermissionBasedMcpServer.d.ts +65 -0
  27. package/dist/server/createPermissionBasedMcpServer.d.ts.map +1 -0
  28. package/dist/session/ClientResourceCache.d.ts +34 -3
  29. package/dist/session/ClientResourceCache.d.ts.map +1 -1
  30. package/dist/types/index.d.ts +229 -0
  31. package/dist/types/index.d.ts.map +1 -1
  32. package/package.json +7 -2
package/dist/index.js CHANGED
@@ -1,10 +1,16 @@
1
- import { z as f } from "zod";
2
- import p from "fastify";
3
- import A from "@fastify/cors";
4
- import { randomUUID as m } from "node:crypto";
5
- import { StreamableHTTPServerTransport as w } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6
- import { isInitializeRequest as b } from "@modelcontextprotocol/sdk/types.js";
7
- const g = {
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 S {
21
+ class q {
16
22
  constructor(e = {}) {
17
23
  this.keys = {
18
- dynamic: e.keys?.dynamic ?? g.dynamic,
19
- toolsets: e.keys?.toolsets ?? g.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((i) => i.trim()).filter((i) => i.length > 0), o = new Set(Object.keys(s)), r = [];
28
- for (const i of t)
29
- o.has(i) ? r.push(i) : console.warn(
30
- `Invalid toolset '${i}' ignored. Available: ${Array.from(
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 r;
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 r = s[o];
40
- r && (r.modules || []).forEach((i) => t.add(i));
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 t = this.getModulesForToolSets(e, s);
68
- return !t || t.length === 0 ? {
69
- isValid: !1,
70
- error: `No modules found for toolsets: ${e.join(", ")}`
71
- } : { isValid: !0, modules: t };
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 x {
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 r = this.catalog[o];
132
- if (r && (Array.isArray(r.tools) && r.tools.length > 0 && t.push(...r.tools), Array.isArray(r.modules) && r.modules.length > 0))
133
- for (const i of r.modules) {
134
- const l = this.moduleLoaders[i];
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 n = await l(s);
138
- Array.isArray(n) && n.length > 0 && t.push(...n);
139
- } catch (n) {
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 '${i}' failed for toolset '${o}':`,
142
- n
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 T extends Error {
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 v {
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 T(
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 T(
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 C {
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 v({ namespaceWithToolset: !0 });
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
- async enableToolset(e) {
218
- const s = this.resolver.validateToolsetName(e);
219
- if (!s.isValid || !s.sanitized)
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: s.error || "Unknown validation error"
258
+ message: t.error || "Unknown validation error"
223
259
  };
224
- const t = s.sanitized;
225
- if (this.activeToolsets.has(t))
260
+ const o = t.sanitized;
261
+ if (this.activeToolsets.has(o))
226
262
  return {
227
263
  success: !1,
228
- message: `Toolset '${t}' is already enabled.`
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 o = await this.resolver.resolveToolsForToolsets(
232
- [t],
271
+ const l = await this.resolver.resolveToolsForToolsets(
272
+ [o],
233
273
  this.context
234
274
  );
235
- if (this.exposurePolicy?.allowlist && !this.exposurePolicy.allowlist.includes(t))
236
- return {
237
- success: !1,
238
- message: `Toolset '${t}' is not allowed by policy.`
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
- this.registerDirectTools(r, t);
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 '${t}' enabled successfully. Registered ${o?.length ?? 0} tools.`
285
+ message: `Toolset '${o}' enabled successfully. Registered ${l?.length ?? 0} tools.`
269
286
  };
270
- } catch (o) {
271
- return this.activeToolsets.delete(t), {
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 '${t}': ${o instanceof Error ? o.message : "Unknown error"}`
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
- if (!this.activeToolsets.has(t))
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 r of e)
374
+ for (const n of e)
317
375
  try {
318
- const i = await this.enableToolset(r);
319
- s.push({ name: r, ...i });
320
- } catch (i) {
376
+ const l = await this.enableToolset(n, !0);
377
+ s.push({ name: n, ...l });
378
+ } catch (l) {
321
379
  s.push({
322
- name: r,
380
+ name: n,
323
381
  success: !1,
324
- message: i instanceof Error ? i.message : "Unknown error",
382
+ message: l instanceof Error ? l.message : "Unknown error",
325
383
  code: "E_INTERNAL"
326
384
  });
327
385
  }
328
- const t = s.every((r) => r.success), o = t ? "All toolsets enabled" : "Some toolsets failed to enable";
329
- if (s.length > 0)
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 I(a, e, s) {
355
- const t = s?.mode ?? "DYNAMIC";
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: f.string().describe("Toolset name") },
402
+ { name: w.string().describe("Toolset name") },
360
403
  async (o) => {
361
- const { name: r } = o, i = await e.enableToolset(r);
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
- ), a.tool(
409
+ ), r.tool(
367
410
  "disable_toolset",
368
411
  "Disable a toolset by name (state only)",
369
- { name: f.string().describe("Toolset name") },
412
+ { name: w.string().describe("Toolset name") },
370
413
  async (o) => {
371
- const { name: r } = o, i = await e.disableToolset(r);
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
- ), t === "DYNAMIC" && (a.tool(
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(), r = e.getStatus().toolsetToTools, i = o.map((l) => {
382
- const n = e.getToolsetDefinition(l);
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: n ? {
387
- name: n.name,
388
- description: n.description,
389
- modules: n.modules ?? [],
390
- decisionCriteria: n.decisionCriteria ?? void 0
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: r[l] ?? []
435
+ tools: i[l] ?? []
393
436
  };
394
437
  });
395
438
  return {
396
439
  content: [
397
- { type: "text", text: JSON.stringify({ toolsets: i }) }
440
+ { type: "text", text: JSON.stringify({ toolsets: n }) }
398
441
  ]
399
442
  };
400
443
  }
401
- ), a.tool(
444
+ ), r.tool(
402
445
  "describe_toolset",
403
446
  "Describe a toolset with definition, active status and tools",
404
- { name: f.string().describe("Toolset name") },
447
+ { name: w.string().describe("Toolset name") },
405
448
  async (o) => {
406
- const { name: r } = o, i = e.getToolsetDefinition(r), l = e.getStatus().toolsetToTools;
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 '${r}'` })
455
+ text: JSON.stringify({ error: `Unknown toolset '${o.name}'` })
413
456
  }
414
457
  ]
415
458
  };
416
- const n = {
417
- key: r,
418
- active: e.isActive(r),
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: l[r] ?? []
468
+ tools: n[o.name] ?? []
426
469
  };
427
470
  return {
428
- content: [{ type: "text", text: JSON.stringify(n) }]
471
+ content: [{ type: "text", text: JSON.stringify(l) }]
429
472
  };
430
473
  }
431
- )), a.tool(
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(), r = {
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(r) }]
484
+ content: [{ type: "text", text: JSON.stringify(i) }]
442
485
  };
443
486
  }
444
487
  );
445
488
  }
446
- class y {
489
+ class T {
447
490
  constructor(e) {
448
- this.toolsetValidator = new S();
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 x({
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 v({
497
+ const o = new P({
455
498
  namespaceWithToolset: e.exposurePolicy?.namespaceToolsWithSetKey ?? !0
456
499
  });
457
- this.manager = new C({
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 && I(e.server, this.manager, { mode: this.mode });
465
- const r = t.toolsets;
466
- r === "ALL" ? this.manager.enableToolsets(this.resolver.getAvailableToolsets()) : Array.isArray(r) && r.length > 0 && this.manager.enableToolsets(r);
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 r of t) {
477
- const { isValid: i, sanitized: l, error: n } = this.toolsetValidator.validateToolsetName(r, s);
478
- i && l ? o.push(l) : n && console.warn(n);
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: r, sanitized: i, error: l } = this.toolsetValidator.validateToolsetName(o, s);
493
- r && i ? t.push(i) : l && console.warn(l);
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
- class M {
584
+ var p, b;
585
+ class $ {
511
586
  constructor(e = {}) {
512
- this.storage = /* @__PURE__ */ new Map(), this.maxSize = e.maxSize ?? 1e3, this.ttlMs = e.ttlMs ?? 1e3 * 60 * 60;
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.delete(e);
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
- stop() {
538
- this.pruneInterval && (clearInterval(this.pruneInterval), this.pruneInterval = void 0);
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 [s, t] of this.storage.entries())
547
- e - t.lastAccessed > this.ttlMs && this.delete(s);
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
- class L {
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 M(), this.defaultManager = e, this.createBundle = s, this.options = {
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 ?? p({ logger: this.options.logger });
564
- this.options.cors && await e.register(A, { origin: !0 });
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 r = t.headers["mcp-client-id"]?.trim(), i = r && r.length > 0 ? r : `anon-${m()}`, l = !i.startsWith("anon-");
579
- let n = l ? this.clientCache.get(i) : null;
580
- if (!n) {
581
- const h = this.createBundle(), u = h.sessions;
582
- n = {
583
- server: h.server,
584
- orchestrator: h.orchestrator,
585
- sessions: u instanceof Map ? u : /* @__PURE__ */ new Map()
586
- }, l && this.clientCache.set(i, n);
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 && n.sessions.get(c))
591
- d = n.sessions.get(c);
592
- else if (!c && b(t.body)) {
593
- const h = m();
594
- d = new w({
595
- sessionIdGenerator: () => h,
596
- onsessioninitialized: (u) => {
597
- n.sessions.set(u, 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,
723
+ onsessioninitialized: (g) => {
724
+ a.sessions.set(g, d);
598
725
  }
599
726
  });
600
727
  try {
601
- await n.server.connect(d);
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 && n.sessions.delete(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 r = t.headers["mcp-client-id"]?.trim(), i = r && r.length > 0 ? r : "";
626
- if (!i)
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(i);
755
+ const l = this.clientCache.get(n);
629
756
  if (!l)
630
757
  return o.code(400), "Invalid or expired client";
631
- const n = t.headers["mcp-session-id"];
632
- if (!n)
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(n);
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 r = t.headers["mcp-client-id"]?.trim(), i = r && r.length > 0 ? r : "", l = t.headers["mcp-session-id"];
640
- if (!i || !l)
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 n = this.clientCache.get(i), c = n?.sessions.get(l);
650
- if (!n || !c)
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 ? n.sessions.delete(c.sessionId) : n.sessions.delete(l);
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 O(a) {
674
- const e = a.startup?.mode ?? "DYNAMIC";
675
- if (typeof a.createServer != "function")
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 = a.createServer(), t = (n) => typeof n?.server?.notification == "function", o = (n) => typeof n?.notifyToolsListChanged == "function", r = async (n) => {
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(n)) {
680
- await n.server.notification({
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(n) && await n.notifyToolsListChanged();
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
- }, i = new y({
837
+ }, n = new T({
689
838
  server: s,
690
- catalog: a.catalog,
691
- moduleLoaders: a.moduleLoaders,
692
- exposurePolicy: a.exposurePolicy,
693
- context: a.context,
694
- notifyToolsListChanged: async () => r(s),
695
- startup: a.startup,
696
- registerMetaTools: a.registerMetaTools !== void 0 ? a.registerMetaTools : e === "DYNAMIC"
697
- }), l = new L(
698
- i.getManager(),
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: i };
702
- const n = a.createServer(), c = new y({
703
- server: n,
704
- catalog: a.catalog,
705
- moduleLoaders: a.moduleLoaders,
706
- exposurePolicy: a.exposurePolicy,
707
- context: a.context,
708
- notifyToolsListChanged: async () => r(n),
709
- startup: a.startup,
710
- registerMetaTools: a.registerMetaTools !== void 0 ? a.registerMetaTools : e === "DYNAMIC"
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: n, orchestrator: c };
861
+ return { server: a, orchestrator: c };
713
862
  },
714
- a.http,
715
- a.configSchema
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
- O as createMcpServer
1430
+ ge as createMcpServer,
1431
+ pe as createPermissionBasedMcpServer
729
1432
  };
730
1433
  //# sourceMappingURL=index.js.map