toolception
Version:
Dynamic MCP server toolkit for runtime toolset management with Fastify transport and meta-tools
731 lines (730 loc) • 23.3 kB
JavaScript
import { z as f } from "zod";
import p from "fastify";
import A from "@fastify/cors";
import { randomUUID as m } from "node:crypto";
import { StreamableHTTPServerTransport as w } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest as b } from "@modelcontextprotocol/sdk/types.js";
const g = {
dynamic: [
"dynamic-tool-discovery",
"dynamicToolDiscovery",
"DYNAMIC_TOOL_DISCOVERY"
],
toolsets: ["tool-sets", "toolSets", "FMP_TOOL_SETS"]
};
class S {
constructor(e = {}) {
this.keys = {
dynamic: e.keys?.dynamic ?? g.dynamic,
toolsets: e.keys?.toolsets ?? g.toolsets
};
}
resolveMode(e, s) {
return this.isDynamicEnabled(s) ? "DYNAMIC" : this.getToolsetsString(s) ? "STATIC" : this.isDynamicEnabled(e) ? "DYNAMIC" : this.getToolsetsString(e) ? "STATIC" : null;
}
parseCommaSeparatedToolSets(e, s) {
if (!e || typeof e != "string") return [];
const t = e.split(",").map((i) => i.trim()).filter((i) => i.length > 0), o = new Set(Object.keys(s)), r = [];
for (const i of t)
o.has(i) ? r.push(i) : console.warn(
`Invalid toolset '${i}' ignored. Available: ${Array.from(
o
).join(", ")}`
);
return r;
}
getModulesForToolSets(e, s) {
const t = /* @__PURE__ */ new Set();
for (const o of e) {
const r = s[o];
r && (r.modules || []).forEach((i) => t.add(i));
}
return Array.from(t);
}
validateToolsetName(e, s) {
if (!e || typeof e != "string")
return {
isValid: !1,
error: `Invalid toolset name provided. Must be a non-empty string. Available toolsets: ${Object.keys(
s
).join(", ")}`
};
const t = e.trim();
return t.length === 0 ? {
isValid: !1,
error: `Empty toolset name provided. Available toolsets: ${Object.keys(
s
).join(", ")}`
} : s[t] ? { isValid: !0, sanitized: t } : {
isValid: !1,
error: `Toolset '${t}' not found. Available toolsets: ${Object.keys(
s
).join(", ")}`
};
}
validateToolsetModules(e, s) {
try {
const t = this.getModulesForToolSets(e, s);
return !t || t.length === 0 ? {
isValid: !1,
error: `No modules found for toolsets: ${e.join(", ")}`
} : { isValid: !0, modules: t };
} catch (t) {
return {
isValid: !1,
error: `Error resolving modules for ${e.join(", ")}: ${t instanceof Error ? t.message : "Unknown error"}`
};
}
}
isDynamicEnabled(e) {
if (!e) return !1;
for (const s of this.keys.dynamic) {
const t = e[s];
if (t === !0 || typeof t == "string" && t.trim().toLowerCase() === "true")
return !0;
}
return !1;
}
getToolsetsString(e) {
if (e)
for (const s of this.keys.toolsets) {
const t = e[s];
if (typeof t == "string" && t.trim().length > 0)
return t;
}
}
}
class x {
constructor(e) {
this.catalog = e.catalog, this.moduleLoaders = e.moduleLoaders ?? {};
}
getAvailableToolsets() {
return Object.keys(this.catalog);
}
getToolsetDefinition(e) {
return this.catalog[e];
}
validateToolsetName(e) {
if (!e || typeof e != "string")
return {
isValid: !1,
error: `Invalid toolset name provided. Must be a non-empty string. Available toolsets: ${this.getAvailableToolsets().join(
", "
)}`
};
const s = e.trim();
return s.length === 0 ? {
isValid: !1,
error: `Empty toolset name provided. Available toolsets: ${this.getAvailableToolsets().join(
", "
)}`
} : this.catalog[s] ? { isValid: !0, sanitized: s } : {
isValid: !1,
error: `Toolset '${s}' not found. Available toolsets: ${this.getAvailableToolsets().join(
", "
)}`
};
}
async resolveToolsForToolsets(e, s) {
const t = [];
for (const o of e) {
const r = this.catalog[o];
if (r && (Array.isArray(r.tools) && r.tools.length > 0 && t.push(...r.tools), Array.isArray(r.modules) && r.modules.length > 0))
for (const i of r.modules) {
const l = this.moduleLoaders[i];
if (l)
try {
const n = await l(s);
Array.isArray(n) && n.length > 0 && t.push(...n);
} catch (n) {
console.warn(
`Module loader '${i}' failed for toolset '${o}':`,
n
);
}
}
}
return t;
}
}
class T extends Error {
constructor(e, s, t, o) {
super(e), this.name = "ToolingError", this.code = s, this.details = t;
}
}
class v {
constructor(e = {}) {
this.names = /* @__PURE__ */ new Set(), this.toolsetToNames = /* @__PURE__ */ new Map(), this.options = {
namespaceWithToolset: e.namespaceWithToolset ?? !0
};
}
getSafeName(e, s) {
return !this.options.namespaceWithToolset || s.startsWith(`${e}.`) ? s : `${e}.${s}`;
}
has(e) {
return this.names.has(e);
}
add(e) {
if (this.names.has(e))
throw new T(
`Tool name collision: '${e}' already registered`,
"E_TOOL_NAME_CONFLICT"
);
this.names.add(e);
}
addForToolset(e, s) {
this.add(s);
const t = this.toolsetToNames.get(e) ?? /* @__PURE__ */ new Set();
t.add(s), this.toolsetToNames.set(e, t);
}
mapAndValidate(e, s) {
return s.map((t) => {
const o = this.getSafeName(e, t.name);
if (this.has(o))
throw new T(
`Tool name collision for '${o}'`,
"E_TOOL_NAME_CONFLICT"
);
return { ...t, name: o };
});
}
list() {
return Array.from(this.names);
}
listByToolset() {
const e = {};
for (const [s, t] of this.toolsetToNames.entries())
e[s] = Array.from(t);
return e;
}
}
class C {
constructor(e) {
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 });
}
getAvailableToolsets() {
return this.resolver.getAvailableToolsets();
}
getActiveToolsets() {
return Array.from(this.activeToolsets);
}
getToolsetDefinition(e) {
return this.resolver.getToolsetDefinition(e);
}
isActive(e) {
return this.activeToolsets.has(e);
}
async enableToolset(e) {
const s = this.resolver.validateToolsetName(e);
if (!s.isValid || !s.sanitized)
return {
success: !1,
message: s.error || "Unknown validation error"
};
const t = s.sanitized;
if (this.activeToolsets.has(t))
return {
success: !1,
message: `Toolset '${t}' is already enabled.`
};
try {
const o = await this.resolver.resolveToolsForToolsets(
[t],
this.context
);
if (this.exposurePolicy?.allowlist && !this.exposurePolicy.allowlist.includes(t))
return {
success: !1,
message: `Toolset '${t}' is not allowed by policy.`
};
if (this.exposurePolicy?.denylist && this.exposurePolicy.denylist.includes(t))
return {
success: !1,
message: `Toolset '${t}' is denied by policy.`
};
if (this.exposurePolicy?.maxActiveToolsets !== void 0 && this.activeToolsets.size + 1 > this.exposurePolicy.maxActiveToolsets)
return this.exposurePolicy.onLimitExceeded?.(
[t],
Array.from(this.activeToolsets)
), {
success: !1,
message: `Activation exceeds maxActiveToolsets (${this.exposurePolicy.maxActiveToolsets}).`
};
if (o && o.length > 0) {
const r = this.toolRegistry.mapAndValidate(
t,
o
);
this.registerDirectTools(r, t);
}
this.activeToolsets.add(t);
try {
await this.onToolsListChanged?.();
} catch (r) {
console.warn("Failed to send tool list change notification:", r);
}
return {
success: !0,
message: `Toolset '${t}' enabled successfully. Registered ${o?.length ?? 0} tools.`
};
} catch (o) {
return this.activeToolsets.delete(t), {
success: !1,
message: `Failed to enable toolset '${t}': ${o instanceof Error ? o.message : "Unknown error"}`
};
}
}
async disableToolset(e) {
const s = this.resolver.validateToolsetName(e);
if (!s.isValid || !s.sanitized) {
const o = Array.from(this.activeToolsets).join(", ") || "none";
return {
success: !1,
message: `${s.error || "Unknown validation error"} Active toolsets: ${o}`
};
}
const t = s.sanitized;
if (!this.activeToolsets.has(t))
return {
success: !1,
message: `Toolset '${t}' is not currently active. Active toolsets: ${Array.from(this.activeToolsets).join(", ") || "none"}`
};
this.activeToolsets.delete(t);
try {
await this.onToolsListChanged?.();
} catch (o) {
console.warn("Failed to send tool list change notification:", o);
}
return {
success: !0,
message: `Toolset '${t}' disabled successfully. Individual tools remain registered due to MCP limitations.`
};
}
getStatus() {
return {
availableToolsets: this.getAvailableToolsets(),
activeToolsets: this.getActiveToolsets(),
registeredModules: [],
totalToolsets: this.getAvailableToolsets().length,
activeCount: this.activeToolsets.size,
tools: this.toolRegistry.list(),
toolsetToTools: this.toolRegistry.listByToolset()
};
}
async enableToolsets(e) {
const s = [];
for (const r of e)
try {
const i = await this.enableToolset(r);
s.push({ name: r, ...i });
} catch (i) {
s.push({
name: r,
success: !1,
message: i instanceof Error ? i.message : "Unknown error",
code: "E_INTERNAL"
});
}
const t = s.every((r) => r.success), o = t ? "All toolsets enabled" : "Some toolsets failed to enable";
if (s.length > 0)
try {
await this.onToolsListChanged?.();
} catch {
}
return { success: t, results: s, message: o };
}
registerDirectTools(e, s) {
for (const t of e)
try {
this.server.tool(
t.name,
t.description,
t.inputSchema,
async (o) => await t.handler(o)
), s ? this.toolRegistry.addForToolset(s, t.name) : this.toolRegistry.add(t.name);
} catch (o) {
throw console.error(`Failed to register direct tool '${t.name}':`, o), o;
}
}
async enableAllToolsets() {
const e = this.getAvailableToolsets();
return this.enableToolsets(e);
}
}
function I(a, e, s) {
const t = s?.mode ?? "DYNAMIC";
a.tool(
"enable_toolset",
"Enable a toolset by name",
{ name: f.string().describe("Toolset name") },
async (o) => {
const { name: r } = o, i = await e.enableToolset(r);
return {
content: [{ type: "text", text: JSON.stringify(i) }]
};
}
), a.tool(
"disable_toolset",
"Disable a toolset by name (state only)",
{ name: f.string().describe("Toolset name") },
async (o) => {
const { name: r } = o, i = await e.disableToolset(r);
return {
content: [{ type: "text", text: JSON.stringify(i) }]
};
}
), t === "DYNAMIC" && (a.tool(
"list_toolsets",
"List available toolsets with active status and definitions",
{},
async () => {
const o = e.getAvailableToolsets(), r = e.getStatus().toolsetToTools, i = o.map((l) => {
const n = e.getToolsetDefinition(l);
return {
key: l,
active: e.isActive(l),
definition: n ? {
name: n.name,
description: n.description,
modules: n.modules ?? [],
decisionCriteria: n.decisionCriteria ?? void 0
} : null,
tools: r[l] ?? []
};
});
return {
content: [
{ type: "text", text: JSON.stringify({ toolsets: i }) }
]
};
}
), a.tool(
"describe_toolset",
"Describe a toolset with definition, active status and tools",
{ name: f.string().describe("Toolset name") },
async (o) => {
const { name: r } = o, i = e.getToolsetDefinition(r), l = e.getStatus().toolsetToTools;
if (!i)
return {
content: [
{
type: "text",
text: JSON.stringify({ error: `Unknown toolset '${r}'` })
}
]
};
const n = {
key: r,
active: e.isActive(r),
definition: {
name: i.name,
description: i.description,
modules: i.modules ?? [],
decisionCriteria: i.decisionCriteria ?? void 0
},
tools: l[r] ?? []
};
return {
content: [{ type: "text", text: JSON.stringify(n) }]
};
}
)), a.tool(
"list_tools",
"List currently registered tool names (best effort)",
{},
async () => {
const o = e.getStatus(), r = {
tools: o.tools,
toolsetToTools: o.toolsetToTools
};
return {
content: [{ type: "text", text: JSON.stringify(r) }]
};
}
);
}
class y {
constructor(e) {
this.toolsetValidator = new S();
const s = e.startup ?? {}, t = this.resolveStartupConfig(s, e.catalog);
this.mode = t.mode, this.resolver = new x({
catalog: e.catalog,
moduleLoaders: e.moduleLoaders
});
const o = new v({
namespaceWithToolset: e.exposurePolicy?.namespaceToolsWithSetKey ?? !0
});
this.manager = new C({
server: e.server,
resolver: this.resolver,
context: e.context,
onToolsListChanged: e.notifyToolsListChanged,
exposurePolicy: e.exposurePolicy,
toolRegistry: o
}), e.registerMetaTools !== !1 && I(e.server, this.manager, { mode: this.mode });
const r = t.toolsets;
r === "ALL" ? this.manager.enableToolsets(this.resolver.getAvailableToolsets()) : Array.isArray(r) && r.length > 0 && this.manager.enableToolsets(r);
}
resolveStartupConfig(e, s) {
if (e.mode) {
if (e.mode === "DYNAMIC" && e.toolsets)
return console.warn("startup.toolsets provided but ignored in DYNAMIC mode"), { mode: "DYNAMIC" };
if (e.mode === "STATIC") {
if (e.toolsets === "ALL")
return { mode: "STATIC", toolsets: "ALL" };
const t = Array.isArray(e.toolsets) ? e.toolsets : [], o = [];
for (const r of t) {
const { isValid: i, sanitized: l, error: n } = this.toolsetValidator.validateToolsetName(r, s);
i && l ? o.push(l) : n && console.warn(n);
}
if (t.length > 0 && o.length === 0)
throw new Error(
"STATIC mode requires valid toolsets or 'ALL'; none were valid"
);
return { mode: "STATIC", toolsets: o };
}
return { mode: e.mode };
}
if (e.toolsets === "ALL") return { mode: "STATIC", toolsets: "ALL" };
if (Array.isArray(e.toolsets) && e.toolsets.length > 0) {
const t = [];
for (const o of e.toolsets) {
const { isValid: r, sanitized: i, error: l } = this.toolsetValidator.validateToolsetName(o, s);
r && i ? t.push(i) : l && console.warn(l);
}
if (t.length === 0)
throw new Error(
"STATIC mode requires valid toolsets or 'ALL'; none were valid"
);
return { mode: "STATIC", toolsets: t };
}
return { mode: "DYNAMIC" };
}
getMode() {
return this.mode;
}
getManager() {
return this.manager;
}
}
class M {
constructor(e = {}) {
this.storage = /* @__PURE__ */ new Map(), this.maxSize = e.maxSize ?? 1e3, this.ttlMs = e.ttlMs ?? 1e3 * 60 * 60;
const s = e.pruneIntervalMs ?? 1e3 * 60 * 10;
this.pruneInterval = setInterval(() => this.pruneExpired(), s);
}
getEntryCount() {
return this.storage.size;
}
getMaxSize() {
return this.maxSize;
}
getTtl() {
return this.ttlMs;
}
get(e) {
const s = this.storage.get(e);
return s ? Date.now() - s.lastAccessed > this.ttlMs ? (this.delete(e), null) : (s.lastAccessed = Date.now(), this.storage.delete(e), this.storage.set(e, s), s.resource) : null;
}
set(e, s) {
this.storage.size >= this.maxSize && this.evictLeastRecentlyUsed();
const t = { resource: s, lastAccessed: Date.now() };
this.storage.set(e, t);
}
delete(e) {
this.storage.delete(e);
}
stop() {
this.pruneInterval && (clearInterval(this.pruneInterval), this.pruneInterval = void 0);
}
evictLeastRecentlyUsed() {
const e = this.storage.keys().next().value;
e && this.delete(e);
}
pruneExpired() {
const e = Date.now();
for (const [s, t] of this.storage.entries())
e - t.lastAccessed > this.ttlMs && this.delete(s);
}
}
class L {
constructor(e, s, t = {}, o) {
this.app = null, this.clientCache = new M(), this.defaultManager = e, this.createBundle = s, this.options = {
host: t.host ?? "0.0.0.0",
port: t.port ?? 3e3,
basePath: t.basePath ?? "/",
cors: t.cors ?? !0,
logger: t.logger ?? !1,
app: t.app
}, this.configSchema = o;
}
async start() {
if (this.app) return;
const e = this.options.app ?? p({ logger: this.options.logger });
this.options.cors && await e.register(A, { origin: !0 });
const s = this.options.basePath.endsWith("/") ? this.options.basePath.slice(0, -1) : this.options.basePath;
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 ?? {
$schema: "https://json-schema.org/draft/2020-12/schema",
title: "MCP Session Configuration",
description: "Schema for the /mcp endpoint configuration",
type: "object",
properties: {},
required: [],
"x-mcp-version": "1.0",
"x-query-style": "dot+bracket"
})), e.post(
`${s}/mcp`,
async (t, o) => {
const r = t.headers["mcp-client-id"]?.trim(), i = r && r.length > 0 ? r : `anon-${m()}`, l = !i.startsWith("anon-");
let n = l ? this.clientCache.get(i) : null;
if (!n) {
const h = this.createBundle(), u = h.sessions;
n = {
server: h.server,
orchestrator: h.orchestrator,
sessions: u instanceof Map ? u : /* @__PURE__ */ new Map()
}, l && this.clientCache.set(i, n);
}
const c = t.headers["mcp-session-id"];
let d;
if (c && n.sessions.get(c))
d = n.sessions.get(c);
else if (!c && b(t.body)) {
const h = m();
d = new w({
sessionIdGenerator: () => h,
onsessioninitialized: (u) => {
n.sessions.set(u, d);
}
});
try {
await n.server.connect(d);
} catch {
return o.code(500), {
jsonrpc: "2.0",
error: { code: -32603, message: "Error initializing server." },
id: null
};
}
d.onclose = () => {
d?.sessionId && n.sessions.delete(d.sessionId);
};
} else
return o.code(400), {
jsonrpc: "2.0",
error: { code: -32e3, message: "Session not found or expired" },
id: null
};
return await d.handleRequest(
t.raw,
o.raw,
t.body
), o;
}
), e.get(`${s}/mcp`, async (t, o) => {
const r = t.headers["mcp-client-id"]?.trim(), i = r && r.length > 0 ? r : "";
if (!i)
return o.code(400), "Missing mcp-client-id";
const l = this.clientCache.get(i);
if (!l)
return o.code(400), "Invalid or expired client";
const n = t.headers["mcp-session-id"];
if (!n)
return o.code(400), "Missing mcp-session-id";
const c = l.sessions.get(n);
return c ? (await c.handleRequest(t.raw, o.raw), o) : (o.code(400), "Invalid or expired session ID");
}), e.delete(
`${s}/mcp`,
async (t, o) => {
const r = t.headers["mcp-client-id"]?.trim(), i = r && r.length > 0 ? r : "", l = t.headers["mcp-session-id"];
if (!i || !l)
return o.code(400), {
jsonrpc: "2.0",
error: {
code: -32600,
message: "Missing mcp-client-id or mcp-session-id header"
},
id: null
};
const n = this.clientCache.get(i), c = n?.sessions.get(l);
if (!n || !c)
return o.code(404), {
jsonrpc: "2.0",
error: { code: -32e3, message: "Session not found or expired" },
id: null
};
try {
if (typeof c.close == "function")
try {
await c.close();
} catch {
}
} finally {
c?.sessionId ? n.sessions.delete(c.sessionId) : n.sessions.delete(l);
}
return o.code(204).send(), o;
}
), this.options.app || await e.listen({ host: this.options.host, port: this.options.port }), this.app = e;
}
async stop() {
this.app && (this.options.app || await this.app.close(), this.app = null);
}
}
async function O(a) {
const e = a.startup?.mode ?? "DYNAMIC";
if (typeof a.createServer != "function")
throw new Error("createMcpServer: `createServer` (factory) is required");
const s = a.createServer(), t = (n) => typeof n?.server?.notification == "function", o = (n) => typeof n?.notifyToolsListChanged == "function", r = async (n) => {
try {
if (t(n)) {
await n.server.notification({
method: "notifications/tools/list_changed"
});
return;
}
o(n) && await n.notifyToolsListChanged();
} catch {
}
}, i = new y({
server: s,
catalog: a.catalog,
moduleLoaders: a.moduleLoaders,
exposurePolicy: a.exposurePolicy,
context: a.context,
notifyToolsListChanged: async () => r(s),
startup: a.startup,
registerMetaTools: a.registerMetaTools !== void 0 ? a.registerMetaTools : e === "DYNAMIC"
}), l = new L(
i.getManager(),
() => {
if (e === "STATIC")
return { server: s, orchestrator: i };
const n = a.createServer(), c = new y({
server: n,
catalog: a.catalog,
moduleLoaders: a.moduleLoaders,
exposurePolicy: a.exposurePolicy,
context: a.context,
notifyToolsListChanged: async () => r(n),
startup: a.startup,
registerMetaTools: a.registerMetaTools !== void 0 ? a.registerMetaTools : e === "DYNAMIC"
});
return { server: n, orchestrator: c };
},
a.http,
a.configSchema
);
return {
server: s,
start: async () => {
await l.start();
},
close: async () => {
await l.stop();
}
};
}
export {
O as createMcpServer
};
//# sourceMappingURL=index.js.map