🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

dynmcp

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

dynmcp - npm Package Compare versions

Comparing version
0.2.0
to
0.3.0
+1077
-181
dist/index.cjs

@@ -26,3 +26,3 @@ "use strict";

// src/cli.ts
var import_node_process6 = __toESM(require("process"), 1);
var import_node_process7 = __toESM(require("process"), 1);
var import_commander = require("commander");

@@ -33,3 +33,3 @@

name: "dynmcp",
version: "0.2.0",
version: "0.3.0",
description: "Dynamic MCP context management tool for AI MCP-enabled agents and clients.",

@@ -114,2 +114,3 @@ author: "Brandon Burrus <brandon@burrus.io>",

"@types/node": "^25.9.0",
"@vitest/coverage-v8": "^4.1.6",
husky: "^9.1.7",

@@ -128,4 +129,4 @@ tsup: "^8.5.1",

// src/proxy/index.ts
var import_node_process5 = __toESM(require("process"), 1);
var import_stdio2 = require("@modelcontextprotocol/sdk/client/stdio.js");
var import_node_process6 = __toESM(require("process"), 1);
var import_stdio3 = require("@modelcontextprotocol/sdk/client/stdio.js");

@@ -392,29 +393,35 @@ // src/config/schema.ts

// src/proxy/transport-factory.ts
var import_stdio = require("@modelcontextprotocol/sdk/client/stdio.js");
var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
var import_sse = require("@modelcontextprotocol/sdk/client/sse.js");
function createTransport(config) {
switch (config.transport) {
case "stdio":
return new import_stdio.StdioClientTransport({
command: config.command,
args: config.args,
env: config.env
});
case "streamable-http":
return new import_streamableHttp.StreamableHTTPClientTransport(
new URL(config.url),
config.headers ? { requestInit: { headers: config.headers } } : void 0
);
case "sse":
return new import_sse.SSEClientTransport(
new URL(config.url),
config.headers ? { requestInit: { headers: config.headers } } : void 0
);
default: {
const _exhaustive = config;
return _exhaustive;
// src/proxy/orchestrator.ts
var import_node_process4 = __toESM(require("process"), 1);
// src/proxy/capability-aggregator.ts
function aggregateCapabilities(upstreams) {
const aggregated = {
tools: { listChanged: true }
};
for (const caps of upstreams) {
if (caps === void 0) continue;
if (caps.resources !== void 0) {
aggregated.resources ??= {};
if (caps.resources.subscribe === true) {
aggregated.resources.subscribe = true;
}
if (caps.resources.listChanged === true) {
aggregated.resources.listChanged = true;
}
}
if (caps.prompts !== void 0) {
aggregated.prompts ??= {};
if (caps.prompts.listChanged === true) {
aggregated.prompts.listChanged = true;
}
}
if (caps.logging !== void 0) {
aggregated.logging ??= {};
}
if (caps.completions !== void 0) {
aggregated.completions ??= {};
}
}
return aggregated;
}

@@ -525,11 +532,247 @@

// src/proxy/notification-forwarder.ts
var NotificationForwarder = class {
constructor(registry, resourceRouter, promptRouter, toolsByMcp, setToolCatalog, namespaced) {
this.registry = registry;
this.resourceRouter = resourceRouter;
this.promptRouter = promptRouter;
this.toolsByMcp = toolsByMcp;
this.setToolCatalog = setToolCatalog;
this.namespaced = namespaced;
}
registry;
resourceRouter;
promptRouter;
toolsByMcp;
setToolCatalog;
namespaced;
hostHandlers = {};
setHostHandlers(handlers) {
this.hostHandlers = handlers;
}
async handleToolsListChanged(mcpName2) {
const client = this.registry.get(mcpName2);
if (client === void 0) return;
const tools = await client.listTools().catch(() => []);
this.toolsByMcp.set(mcpName2, tools);
const rebuilt = this.namespaced ? ToolCatalog.fromGrouped(this.toolsByMcp) : ToolCatalog.fromFlat([...this.toolsByMcp.values()][0] ?? []);
this.setToolCatalog(rebuilt);
await this.hostHandlers.onToolsListChanged?.();
}
async handleResourcesListChanged(mcpName2) {
const router = this.resourceRouter();
const client = this.registry.get(mcpName2);
if (router === null || client === void 0) return;
const [resources, templates] = await Promise.all([
client.listResources().catch(() => []),
client.listResourceTemplates().catch(() => [])
]);
router.setResources(mcpName2, resources);
router.setTemplates(mcpName2, templates);
await this.hostHandlers.onResourcesListChanged?.();
}
async handleResourceUpdated(params) {
await this.hostHandlers.onResourceUpdated?.(params);
}
async handlePromptsListChanged(mcpName2) {
const router = this.promptRouter();
const client = this.registry.get(mcpName2);
if (router === null || client === void 0) return;
const prompts = await client.listPrompts().catch(() => []);
router.setPrompts(mcpName2, prompts);
await this.hostHandlers.onPromptsListChanged?.();
}
/**
* Rewrites the upstream's `logger` field with the originating MCP's name as a
* prefix so the host can attribute log lines, then forwards the message to the
* host's log message handler.
*/
async handleLogMessage(mcpName2, params) {
const handler = this.hostHandlers.onLogMessage;
if (handler === void 0) return;
if (!this.namespaced) {
await handler(params);
return;
}
const prefixed = {
...params,
logger: params.logger === void 0 ? mcpName2 : `${mcpName2}/${params.logger}`
};
await handler(prefixed);
}
};
// src/proxy/prompt-router.ts
var PromptRouter = class {
mcpOrder;
perMcp;
nameOwners = /* @__PURE__ */ new Map();
detectedCollisions = [];
constructor(mcpOrder) {
this.mcpOrder = [...mcpOrder];
this.perMcp = new Map(this.mcpOrder.map((name) => [name, []]));
}
setPrompts(mcpName2, prompts) {
const entry = this.perMcp.get(mcpName2);
if (entry === void 0) {
throw new Error(`PromptRouter: unknown mcp "${mcpName2}"`);
}
this.perMcp.set(mcpName2, [...prompts]);
this.rebuild();
}
aggregatedPrompts() {
const result = [];
for (const mcpName2 of this.mcpOrder) {
const entry = this.perMcp.get(mcpName2);
if (entry !== void 0) {
result.push(...entry);
}
}
return result;
}
ownerOf(promptName) {
return this.nameOwners.get(promptName);
}
collisions() {
return this.detectedCollisions;
}
rebuild() {
this.nameOwners = /* @__PURE__ */ new Map();
const collisions = [];
for (const mcpName2 of this.mcpOrder) {
const prompts = this.perMcp.get(mcpName2);
if (prompts === void 0) continue;
for (const prompt of prompts) {
const existing = this.nameOwners.get(prompt.name);
if (existing === void 0) {
this.nameOwners.set(prompt.name, mcpName2);
} else {
collisions.push({ name: prompt.name, chosen: existing, shadowed: mcpName2 });
}
}
}
this.detectedCollisions = collisions;
}
};
// src/proxy/resource-router.ts
var ResourceRouter = class {
mcpOrder;
perMcp;
uriOwners = /* @__PURE__ */ new Map();
templateOwners = [];
detectedCollisions = [];
constructor(mcpOrder) {
this.mcpOrder = [...mcpOrder];
this.perMcp = new Map(
this.mcpOrder.map((name) => [name, { resources: [], templates: [] }])
);
}
setResources(mcpName2, resources) {
const entry = this.perMcp.get(mcpName2);
if (entry === void 0) {
throw new Error(`ResourceRouter: unknown mcp "${mcpName2}"`);
}
entry.resources = [...resources];
this.rebuild();
}
setTemplates(mcpName2, templates) {
const entry = this.perMcp.get(mcpName2);
if (entry === void 0) {
throw new Error(`ResourceRouter: unknown mcp "${mcpName2}"`);
}
entry.templates = [...templates];
this.rebuild();
}
aggregatedResources() {
const result = [];
for (const mcpName2 of this.mcpOrder) {
const entry = this.perMcp.get(mcpName2);
if (entry !== void 0) {
result.push(...entry.resources);
}
}
return result;
}
aggregatedTemplates() {
const result = [];
for (const mcpName2 of this.mcpOrder) {
const entry = this.perMcp.get(mcpName2);
if (entry !== void 0) {
result.push(...entry.templates);
}
}
return result;
}
/**
* Returns the mcpName that owns the given URI, or undefined if no upstream advertises it.
* Concrete URI matches take precedence over template prefix matches; templates are tried
* in config-file order (first-wins).
*/
ownerOf(uri) {
const concrete = this.uriOwners.get(uri);
if (concrete !== void 0) {
return concrete;
}
for (const { prefix, mcpName: mcpName2 } of this.templateOwners) {
if (prefix.length > 0 && uri.startsWith(prefix)) {
return mcpName2;
}
}
return void 0;
}
collisions() {
return this.detectedCollisions;
}
rebuild() {
this.uriOwners = /* @__PURE__ */ new Map();
this.templateOwners = [];
const collisions = [];
for (const mcpName2 of this.mcpOrder) {
const entry = this.perMcp.get(mcpName2);
if (entry === void 0) continue;
for (const resource of entry.resources) {
const existing = this.uriOwners.get(resource.uri);
if (existing === void 0) {
this.uriOwners.set(resource.uri, mcpName2);
} else {
collisions.push({ uri: resource.uri, chosen: existing, shadowed: mcpName2 });
}
}
for (const template of entry.templates) {
this.templateOwners.push({
prefix: literalPrefixOf(template.uriTemplate),
template,
mcpName: mcpName2
});
}
}
this.detectedCollisions = collisions;
}
};
function literalPrefixOf(uriTemplate) {
const idx = uriTemplate.indexOf("{");
return idx === -1 ? uriTemplate : uriTemplate.slice(0, idx);
}
// src/proxy/upstream-client.ts
var import_node_process3 = __toESM(require("process"), 1);
var import_client = require("@modelcontextprotocol/sdk/client/index.js");
var import_types = require("@modelcontextprotocol/sdk/types.js");
var UpstreamClient = class {
transport;
onTransportError;
notificationHandlers;
serverRequestHandlers;
client = null;
constructor({ name, transport, onTransportError }) {
constructor({
name,
transport,
onTransportError,
notifications,
serverRequests
}) {
this.transport = transport;
this.notificationHandlers = notifications ?? {};
this.serverRequestHandlers = serverRequests ?? {};
this.onTransportError = onTransportError ?? ((error) => {

@@ -542,10 +785,76 @@ import_node_process3.default.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}

this.transport.onerror = this.onTransportError;
this.client = new import_client.Client({ name: "dynamic-discovery-mcp", version: "1.0.0" });
this.client = new import_client.Client(
{ name: "dynamic-discovery-mcp", version: "1.0.0" },
{
capabilities: {
// Declare every client-side capability the proxy may relay on behalf of the host.
// Actual reachability of each feature depends on what the host supports — if the
// host does not support sampling, for instance, the host call returns an error
// which we forward back to the upstream verbatim.
sampling: {},
elicitation: {},
roots: { listChanged: true }
}
}
);
this.registerServerRequestHandlers(this.client);
if (this.notificationHandlers.onToolsListChanged !== void 0) {
this.client.setNotificationHandler(import_types.ToolListChangedNotificationSchema, async () => {
await this.notificationHandlers.onToolsListChanged?.();
});
}
if (this.notificationHandlers.onResourcesListChanged !== void 0) {
this.client.setNotificationHandler(import_types.ResourceListChangedNotificationSchema, async () => {
await this.notificationHandlers.onResourcesListChanged?.();
});
}
if (this.notificationHandlers.onResourceUpdated !== void 0) {
this.client.setNotificationHandler(import_types.ResourceUpdatedNotificationSchema, async (notification) => {
await this.notificationHandlers.onResourceUpdated?.({ uri: notification.params.uri });
});
}
if (this.notificationHandlers.onPromptsListChanged !== void 0) {
this.client.setNotificationHandler(import_types.PromptListChangedNotificationSchema, async () => {
await this.notificationHandlers.onPromptsListChanged?.();
});
}
if (this.notificationHandlers.onLogMessage !== void 0) {
this.client.setNotificationHandler(import_types.LoggingMessageNotificationSchema, async (notification) => {
await this.notificationHandlers.onLogMessage?.(notification.params);
});
}
await this.client.connect(this.transport);
}
async listTools() {
if (this.client === null) {
throw new Error("Client is not connected. Call connect() first.");
async setLoggingLevel(level, options) {
const client = this.requireClient();
await client.setLoggingLevel(level, options);
}
async listPrompts(options) {
const client = this.requireClient();
const result = await client.listPrompts(void 0, options);
return result.prompts;
}
async getPrompt(name, args, options) {
const client = this.requireClient();
const params = { name };
if (args !== void 0) {
params.arguments = args;
}
const result = await this.client.listTools();
return client.getPrompt(params, options);
}
async complete(params, options) {
const client = this.requireClient();
return client.complete(params, options);
}
/**
* Returns the capabilities advertised by the upstream server during initialize.
* Returns `undefined` if the client is not connected, or if the SDK has not yet
* recorded the server's capabilities (e.g. during a partially-completed handshake).
*/
getCapabilities() {
return this.client?.getServerCapabilities();
}
async listTools(options) {
const client = this.requireClient();
const result = await client.listTools(void 0, options);
return result.tools.map((tool) => {

@@ -572,9 +881,29 @@ const upstreamTool = {

}
async callTool(name, input) {
if (this.client === null) {
throw new Error("Client is not connected. Call connect() first.");
}
const result = await this.client.callTool({ name, arguments: input });
async callTool(name, input, options) {
const client = this.requireClient();
const result = await client.callTool({ name, arguments: input }, void 0, options);
return result;
}
async listResources(options) {
const client = this.requireClient();
const result = await client.listResources(void 0, options);
return result.resources;
}
async listResourceTemplates(options) {
const client = this.requireClient();
const result = await client.listResourceTemplates(void 0, options);
return result.resourceTemplates;
}
async readResource(uri, options) {
const client = this.requireClient();
return client.readResource({ uri }, options);
}
async subscribeResource(uri, options) {
const client = this.requireClient();
await client.subscribeResource({ uri }, options);
}
async unsubscribeResource(uri, options) {
const client = this.requireClient();
await client.unsubscribeResource({ uri }, options);
}
async disconnect() {

@@ -587,27 +916,65 @@ if (this.client === null) {

}
/**
* Sends `notifications/roots/list_changed` to the upstream, letting it know that
* the host's set of filesystem roots has changed.
*/
async sendRootsListChanged() {
const client = this.requireClient();
await client.sendRootsListChanged();
}
registerServerRequestHandlers(client) {
if (this.serverRequestHandlers.onCreateMessage !== void 0) {
client.setRequestHandler(
import_types.CreateMessageRequestSchema,
async (request, extra) => {
return this.serverRequestHandlers.onCreateMessage(request.params, {
signal: extra.signal
});
}
);
}
if (this.serverRequestHandlers.onElicitInput !== void 0) {
client.setRequestHandler(
import_types.ElicitRequestSchema,
async (request, extra) => {
return this.serverRequestHandlers.onElicitInput(request.params, {
signal: extra.signal
});
}
);
}
if (this.serverRequestHandlers.onListRoots !== void 0) {
client.setRequestHandler(
import_types.ListRootsRequestSchema,
async (request, extra) => {
return this.serverRequestHandlers.onListRoots(request.params, {
signal: extra.signal
});
}
);
}
}
requireClient() {
if (this.client === null) {
throw new Error("Client is not connected. Call connect() first.");
}
return this.client;
}
};
// src/proxy/orchestrator.ts
var Orchestrator = class {
config;
// src/proxy/upstream-registry.ts
var UpstreamRegistry = class {
clients = /* @__PURE__ */ new Map();
toolCatalog = null;
constructor(config) {
this.config = config;
}
async connect() {
const groups = /* @__PURE__ */ new Map();
async connectAll(entries) {
try {
for (const [mcpName2, { transport }] of this.config.mcps) {
for (const [mcpName2, config] of entries) {
const client = new UpstreamClient({
name: mcpName2,
transport,
onTransportError: (error) => {
this.config.onTransportError?.(mcpName2, error);
}
transport: config.transport,
onTransportError: config.onTransportError,
notifications: config.notifications,
serverRequests: config.serverRequests
});
await client.connect();
const tools = await client.listTools();
this.clients.set(mcpName2, client);
groups.set(mcpName2, tools);
}

@@ -618,4 +985,127 @@ } catch (error) {

}
this.toolCatalog = ToolCatalog.fromGrouped(groups);
}
get(mcpName2) {
return this.clients.get(mcpName2);
}
/**
* Returns the sole connected client. Used by single-MCP (`--`) mode where the
* Orchestrator guarantees there is exactly one upstream. Returns undefined when
* zero or more than one client is connected.
*/
sole() {
if (this.clients.size !== 1) return void 0;
return this.clients.values().next().value;
}
names() {
return [...this.clients.keys()];
}
entries() {
return this.clients.entries();
}
size() {
return this.clients.size;
}
async disconnectAll() {
const disconnections = [...this.clients.values()].map((client) => client.disconnect());
await Promise.all(disconnections);
this.clients.clear();
}
};
// src/proxy/orchestrator.ts
var Orchestrator = class {
config;
registry = new UpstreamRegistry();
toolsByMcp = /* @__PURE__ */ new Map();
resourceRouter = null;
promptRouter = null;
toolCatalog = null;
aggregatedCapabilities = null;
serverRequestForwarders = {};
forwarder;
constructor(config) {
if (!config.namespaced && config.mcps.size !== 1) {
throw new Error(
`Single-MCP (non-namespaced) mode requires exactly one upstream; got ${config.mcps.size}.`
);
}
this.config = config;
this.forwarder = new NotificationForwarder(
this.registry,
() => this.resourceRouter,
() => this.promptRouter,
this.toolsByMcp,
(catalog) => {
this.toolCatalog = catalog;
},
this.config.namespaced
);
}
setNotificationHandlers(handlers) {
this.forwarder.setHostHandlers(handlers);
}
setServerRequestForwarders(forwarders) {
this.serverRequestForwarders = forwarders;
}
async connect() {
const resourceRouter = new ResourceRouter([...this.config.mcps.keys()]);
const promptRouter = new PromptRouter([...this.config.mcps.keys()]);
const upstreamEntries = [
...this.config.mcps
].map(([mcpName2, { transport }]) => [
mcpName2,
{
transport,
onTransportError: (error) => {
this.config.onTransportError?.(mcpName2, error);
},
notifications: {
onToolsListChanged: () => this.forwarder.handleToolsListChanged(mcpName2),
onResourcesListChanged: () => this.forwarder.handleResourcesListChanged(mcpName2),
onResourceUpdated: (params) => this.forwarder.handleResourceUpdated(params),
onPromptsListChanged: () => this.forwarder.handlePromptsListChanged(mcpName2),
onLogMessage: (params) => this.forwarder.handleLogMessage(mcpName2, params)
},
serverRequests: {
onCreateMessage: (params, opts) => this.forwardCreateMessage(params, opts),
onElicitInput: (params, opts) => this.forwardElicitInput(params, opts),
onListRoots: (params, opts) => this.forwardListRoots(params, opts)
}
}
]);
await this.registry.connectAll(upstreamEntries);
const capabilityList = [];
this.toolsByMcp.clear();
for (const [mcpName2, client] of this.registry.entries()) {
const caps = client.getCapabilities();
capabilityList.push(caps);
const tools = await client.listTools();
this.toolsByMcp.set(mcpName2, tools);
if (caps?.resources !== void 0) {
const [resources, templates] = await Promise.all([
client.listResources().catch(() => []),
client.listResourceTemplates().catch(() => [])
]);
resourceRouter.setResources(mcpName2, resources);
resourceRouter.setTemplates(mcpName2, templates);
}
if (caps?.prompts !== void 0) {
const prompts = await client.listPrompts().catch(() => []);
promptRouter.setPrompts(mcpName2, prompts);
}
}
this.toolCatalog = this.config.namespaced ? ToolCatalog.fromGrouped(this.toolsByMcp) : ToolCatalog.fromFlat([...this.toolsByMcp.values()][0] ?? []);
this.aggregatedCapabilities = aggregateCapabilities(capabilityList);
this.resourceRouter = resourceRouter;
this.promptRouter = promptRouter;
logCollisions(resourceRouter, promptRouter);
}
async disconnectAll() {
await this.registry.disconnectAll();
this.toolsByMcp.clear();
this.toolCatalog = null;
this.aggregatedCapabilities = null;
this.resourceRouter = null;
this.promptRouter = null;
}
get catalog() {

@@ -627,126 +1117,491 @@ if (this.toolCatalog === null) {

}
async callTool(namespacedName, input) {
const separatorIndex = namespacedName.indexOf("/");
if (separatorIndex === -1) {
throw new Error(
`Invalid namespaced tool name: "${namespacedName}". Expected format: "mcpName/toolName".`
);
get capabilities() {
if (this.aggregatedCapabilities === null) {
throw new Error("Orchestrator is not connected. Call connect() first.");
}
const mcpName2 = namespacedName.slice(0, separatorIndex);
const toolName = namespacedName.slice(separatorIndex + 1);
const client = this.clients.get(mcpName2);
return this.aggregatedCapabilities;
}
// === Forward-direction request routing ===
async callTool(displayName, input, options) {
if (this.config.namespaced) {
const { mcpName: mcpName2, toolName } = splitNamespacedName(displayName, this.registry.names());
return this.requireClient(mcpName2, "tool").callTool(toolName, input, options);
}
const sole = this.registry.sole();
if (sole === void 0) {
throw new Error("Orchestrator is not connected. Call connect() first.");
}
return sole.callTool(displayName, input, options);
}
listResources() {
return this.requireResourceRouter().aggregatedResources();
}
listResourceTemplates() {
return this.requireResourceRouter().aggregatedTemplates();
}
async readResource(uri, options) {
return this.resolveResourceOwner(uri).readResource(uri, options);
}
async subscribeResource(uri, options) {
await this.resolveResourceOwner(uri).subscribeResource(uri, options);
}
async unsubscribeResource(uri, options) {
await this.resolveResourceOwner(uri).unsubscribeResource(uri, options);
}
listPrompts() {
return this.requirePromptRouter().aggregatedPrompts();
}
async getPrompt(name, args, options) {
return this.resolvePromptOwner(name).getPrompt(name, args, options);
}
async complete(params, options) {
const client = this.resolveCompletionTarget(params.ref);
return client.complete(params, options);
}
// === Broadcasts ===
/**
* Broadcasts a `logging/setLevel` request to every upstream advertising the logging
* capability. Errors from individual upstreams are swallowed so a single misbehaving
* upstream cannot break the broadcast for others; failures are written to stderr.
*/
async setLoggingLevel(level, options) {
await this.broadcastAsync(
(client) => client.getCapabilities()?.logging !== void 0 ? client.setLoggingLevel(level, options) : Promise.resolve(),
"setLoggingLevel"
);
}
/**
* Broadcasts `notifications/roots/list_changed` to every connected upstream — the
* proxy declares roots capability uniformly to all upstreams so every one of them
* may have requested roots and needs to know the list changed.
*/
async broadcastRootsListChanged() {
await this.broadcastAsync((client) => client.sendRootsListChanged(), "sendRootsListChanged");
}
// === Server-initiated request forwarders (upstream → host) ===
async forwardCreateMessage(params, options) {
const handler = this.serverRequestForwarders.onCreateMessage;
if (handler === void 0) {
throw new Error("Proxy does not support sampling: host has not registered a handler.");
}
return handler(params, options);
}
async forwardElicitInput(params, options) {
const handler = this.serverRequestForwarders.onElicitInput;
if (handler === void 0) {
throw new Error("Proxy does not support elicitation: host has not registered a handler.");
}
return handler(params, options);
}
async forwardListRoots(params, options) {
const handler = this.serverRequestForwarders.onListRoots;
if (handler === void 0) {
throw new Error("Proxy does not support roots: host has not registered a handler.");
}
return handler(params, options);
}
// === Internal helpers ===
requireResourceRouter() {
if (this.resourceRouter === null) {
throw new Error("Orchestrator is not connected. Call connect() first.");
}
return this.resourceRouter;
}
requirePromptRouter() {
if (this.promptRouter === null) {
throw new Error("Orchestrator is not connected. Call connect() first.");
}
return this.promptRouter;
}
resolveResourceOwner(uri) {
const owner = this.requireResourceRouter().ownerOf(uri);
if (owner === void 0) {
throw new Error(`Unknown resource URI: "${uri}". No upstream MCP advertises it.`);
}
return this.requireClient(owner, "resource");
}
resolvePromptOwner(name) {
const owner = this.requirePromptRouter().ownerOf(name);
if (owner === void 0) {
throw new Error(`Unknown prompt: "${name}". No upstream MCP advertises it.`);
}
return this.requireClient(owner, "prompt");
}
resolveCompletionTarget(ref) {
if (ref.type === "ref/prompt") {
return this.resolvePromptOwner(ref.name);
}
if (ref.type === "ref/resource") {
return this.resolveResourceOwner(ref.uri);
}
const unknownRef = ref;
throw new Error(`Unsupported completion ref type: "${unknownRef.type}"`);
}
requireClient(mcpName2, role) {
const client = this.registry.get(mcpName2);
if (client === void 0) {
const available = [...this.clients.keys()].sort().join(", ");
throw new Error(`Unknown MCP: "${mcpName2}". Available MCPs: ${available}`);
throw new Error(`Internal error: ${role} owner "${mcpName2}" has no connected client.`);
}
return client.callTool(toolName, input);
return client;
}
async disconnectAll() {
const disconnections = [...this.clients.values()].map((client) => client.disconnect());
await Promise.all(disconnections);
this.clients.clear();
this.toolCatalog = null;
async broadcastAsync(action, label) {
const targets = [];
for (const [mcpName2, client] of this.registry.entries()) {
targets.push(
action(client).catch((error) => {
const message = error instanceof Error ? error.message : String(error);
import_node_process4.default.stderr.write(`dynmcp: ${label} failed for "${mcpName2}": ${message}
`);
})
);
}
await Promise.all(targets);
}
};
function splitNamespacedName(namespacedName, knownMcpNames) {
const separatorIndex = namespacedName.indexOf("/");
if (separatorIndex === -1) {
throw new Error(
`Invalid namespaced tool name: "${namespacedName}". Expected format: "mcpName/toolName".`
);
}
const mcpName2 = namespacedName.slice(0, separatorIndex);
const toolName = namespacedName.slice(separatorIndex + 1);
if (!knownMcpNames.includes(mcpName2)) {
const available = [...knownMcpNames].sort().join(", ");
throw new Error(`Unknown MCP: "${mcpName2}". Available MCPs: ${available}`);
}
return { mcpName: mcpName2, toolName };
}
function logCollisions(resourceRouter, promptRouter) {
for (const collision of resourceRouter.collisions()) {
import_node_process4.default.stderr.write(
`dynmcp: resource URI collision: "${collision.uri}" is provided by "${collision.chosen}" and "${collision.shadowed}"; routing to "${collision.chosen}".
`
);
}
for (const collision of promptRouter.collisions()) {
import_node_process4.default.stderr.write(
`dynmcp: prompt name collision: "${collision.name}" is provided by "${collision.chosen}" and "${collision.shadowed}"; routing to "${collision.chosen}".
`
);
}
}
// src/proxy/server.ts
var import_node_process4 = __toESM(require("process"), 1);
var import_fastmcp = require("fastmcp");
var import_node_process5 = __toESM(require("process"), 1);
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
var import_types2 = require("@modelcontextprotocol/sdk/types.js");
var import_zod3 = require("zod");
var DISCOVER_TOOL_NAME = "discover_tool";
var USE_TOOL_NAME = "use_tool";
var USE_TOOL_DESCRIPTION = "Use a tool that was previously discovered with the discover_tool tool.";
var DISCOVER_TOOL_INPUT_SCHEMA = {
type: "object",
properties: {
tool_name: { type: "string" }
},
required: ["tool_name"]
};
var USE_TOOL_INPUT_SCHEMA = {
type: "object",
properties: {
tool_name: { type: "string" },
tool_input: { type: "object", additionalProperties: true, default: {} }
},
required: ["tool_name"]
};
var DiscoverToolArgsSchema = import_zod3.z.object({ tool_name: import_zod3.z.string() });
var UseToolArgsSchema = import_zod3.z.object({
tool_name: import_zod3.z.string(),
tool_input: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()).default({})
});
var ProxyServer = class {
catalog;
callTool;
constructor({ catalog, callTool }) {
capabilities;
resources;
prompts;
complete;
setLoggingLevelCallback;
onRootsListChangedCallback;
sdkServer = null;
constructor({
catalog,
callTool,
capabilities,
resources,
prompts,
complete,
setLoggingLevel,
onRootsListChanged
}) {
this.catalog = catalog;
this.callTool = callTool;
this.capabilities = capabilities;
this.resources = resources;
this.prompts = prompts;
this.complete = complete;
this.setLoggingLevelCallback = setLoggingLevel;
this.onRootsListChangedCallback = onRootsListChanged;
}
buildServer() {
const server = new import_server.Server(
{
name: "dynamic-discovery-mcp",
version: package_default.version
},
{
capabilities: this.capabilities
}
);
this.registerToolHandlers(server);
if (this.capabilities.resources !== void 0 && this.resources !== void 0) {
this.registerResourceHandlers(server, this.resources);
}
if (this.capabilities.prompts !== void 0 && this.prompts !== void 0) {
this.registerPromptHandlers(server, this.prompts);
}
if (this.capabilities.completions !== void 0 && this.complete !== void 0) {
this.registerCompletionHandler(server, this.complete);
}
if (this.capabilities.logging !== void 0 && this.setLoggingLevelCallback !== void 0) {
this.registerLoggingHandler(server, this.setLoggingLevelCallback);
}
if (this.onRootsListChangedCallback !== void 0) {
const callback = this.onRootsListChangedCallback;
server.setNotificationHandler(import_types2.RootsListChangedNotificationSchema, async () => {
await callback();
});
}
this.sdkServer = server;
return server;
}
/**
* Forwards an upstream-initiated `sampling/createMessage` request to the host. The
* upstream's abort signal is threaded through so cancellation by the upstream
* propagates to the host.
*/
async forwardCreateMessage(params, options) {
const server = this.requireSdkServer();
return server.createMessage(params, options);
}
/**
* Forwards an upstream-initiated `elicitation/create` request to the host.
*/
async forwardElicitInput(params, options) {
const server = this.requireSdkServer();
return server.elicitInput(params, options);
}
/**
* Forwards an upstream-initiated `roots/list` request to the host.
*/
async forwardListRoots(params, options) {
const server = this.requireSdkServer();
return server.listRoots(params, options);
}
requireSdkServer() {
if (this.sdkServer === null) {
throw new Error("ProxyServer is not built. Call buildServer() before forwarding requests.");
}
return this.sdkServer;
}
async start() {
const server = new import_fastmcp.FastMCP({
name: "dynamic-discovery-mcp",
version: package_default.version
});
server.addTool({
name: "discover_tool",
description: this.catalog.discoverToolDescription,
parameters: import_zod3.z.object({ tool_name: import_zod3.z.string() }),
execute: async ({ tool_name }) => {
return this.catalog.getToolDetails(tool_name);
const server = this.buildServer();
const transport = new import_stdio.StdioServerTransport();
import_node_process5.default.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
await server.connect(transport);
}
/**
* Notifies the host that the discover_tool description has changed because an upstream
* emitted `notifications/tools/list_changed`. The host should re-fetch the tools list
* to pick up the regenerated catalog. Silently no-ops if `buildServer()` has not been
* called yet.
*/
async sendToolListChanged() {
if (this.sdkServer !== null) {
await this.sdkServer.sendToolListChanged();
}
}
/**
* Notifies the host that the proxy's aggregated resource list has changed. Silently
* no-ops if `buildServer()` has not been called yet. Errors propagate.
*/
async sendResourceListChanged() {
if (this.sdkServer !== null) {
await this.sdkServer.sendResourceListChanged();
}
}
/**
* Notifies the host that a specific subscribed resource has changed. Silently no-ops
* if `buildServer()` has not been called yet.
*/
async sendResourceUpdated(params) {
if (this.sdkServer !== null) {
await this.sdkServer.sendResourceUpdated(params);
}
}
/**
* Notifies the host that the proxy's aggregated prompt list has changed. Silently
* no-ops if `buildServer()` has not been called yet.
*/
async sendPromptListChanged() {
if (this.sdkServer !== null) {
await this.sdkServer.sendPromptListChanged();
}
}
registerToolHandlers(server) {
server.setRequestHandler(import_types2.ListToolsRequestSchema, async () => ({
tools: [
{
name: DISCOVER_TOOL_NAME,
description: this.catalog().discoverToolDescription,
inputSchema: DISCOVER_TOOL_INPUT_SCHEMA
},
{
name: USE_TOOL_NAME,
description: USE_TOOL_DESCRIPTION,
inputSchema: USE_TOOL_INPUT_SCHEMA
}
]
}));
server.setRequestHandler(
import_types2.CallToolRequestSchema,
async (request, extra) => {
const { name, arguments: rawArgs } = request.params;
const catalog = this.catalog();
if (name === DISCOVER_TOOL_NAME) {
const args = DiscoverToolArgsSchema.parse(rawArgs ?? {});
return {
content: [{ type: "text", text: catalog.getToolDetails(args.tool_name) }]
};
}
if (name === USE_TOOL_NAME) {
const args = UseToolArgsSchema.parse(rawArgs ?? {});
if (!catalog.tools.has(args.tool_name)) {
return {
content: [{ type: "text", text: catalog.getToolDetails(args.tool_name) }]
};
}
return await this.callTool(args.tool_name, args.tool_input, { signal: extra.signal });
}
return {
isError: true,
content: [{ type: "text", text: `Unknown tool: "${name}"` }]
};
}
);
}
registerResourceHandlers(server, callbacks) {
server.setRequestHandler(
import_types2.ListResourcesRequestSchema,
async () => ({
resources: callbacks.listResources()
})
);
server.setRequestHandler(
import_types2.ListResourceTemplatesRequestSchema,
async () => ({
resourceTemplates: callbacks.listResourceTemplates()
})
);
server.setRequestHandler(
import_types2.ReadResourceRequestSchema,
async (request, extra) => {
return callbacks.readResource(request.params.uri, { signal: extra.signal });
}
);
server.setRequestHandler(import_types2.SubscribeRequestSchema, async (request, extra) => {
await callbacks.subscribeResource(request.params.uri, { signal: extra.signal });
return {};
});
server.addTool({
name: "use_tool",
description: "Use a tool that was previously discovered with the discover_tool tool.",
parameters: import_zod3.z.object({
tool_name: import_zod3.z.string(),
tool_input: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()).default({})
}),
execute: async ({ tool_name, tool_input }) => {
if (!this.catalog.tools.has(tool_name)) {
return this.catalog.getToolDetails(tool_name);
}
const result = await this.callTool(tool_name, tool_input);
return result;
server.setRequestHandler(import_types2.UnsubscribeRequestSchema, async (request, extra) => {
await callbacks.unsubscribeResource(request.params.uri, { signal: extra.signal });
return {};
});
}
registerPromptHandlers(server, callbacks) {
server.setRequestHandler(
import_types2.ListPromptsRequestSchema,
async () => ({
prompts: callbacks.listPrompts()
})
);
server.setRequestHandler(
import_types2.GetPromptRequestSchema,
async (request, extra) => {
return callbacks.getPrompt(request.params.name, request.params.arguments, {
signal: extra.signal
});
}
);
}
registerCompletionHandler(server, callback) {
server.setRequestHandler(
import_types2.CompleteRequestSchema,
async (request, extra) => {
return callback(request.params, { signal: extra.signal });
}
);
}
registerLoggingHandler(server, callback) {
server.setRequestHandler(import_types2.SetLevelRequestSchema, async (request, extra) => {
await callback(request.params.level, { signal: extra.signal });
return {};
});
import_node_process4.default.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
await server.start({ transportType: "stdio" });
}
/**
* Forwards a log message from an upstream MCP to the host. Silently no-ops if
* `buildServer()` has not been called yet.
*/
async sendLoggingMessage(params) {
if (this.sdkServer !== null) {
await this.sdkServer.sendLoggingMessage(params);
}
}
};
// src/proxy/transport-factory.ts
var import_stdio2 = require("@modelcontextprotocol/sdk/client/stdio.js");
var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
var import_sse = require("@modelcontextprotocol/sdk/client/sse.js");
function createTransport(config) {
switch (config.transport) {
case "stdio":
return new import_stdio2.StdioClientTransport({
command: config.command,
args: config.args,
env: config.env
});
case "streamable-http":
return new import_streamableHttp.StreamableHTTPClientTransport(
new URL(config.url),
config.headers ? { requestInit: { headers: config.headers } } : void 0
);
case "sse":
return new import_sse.SSEClientTransport(
new URL(config.url),
config.headers ? { requestInit: { headers: config.headers } } : void 0
);
default: {
const _exhaustive = config;
return _exhaustive;
}
}
}
// src/proxy/index.ts
var SINGLE_MCP_NAME = "__default__";
async function startProxy(command, args) {
let isShuttingDown = false;
const transport = new import_stdio2.StdioClientTransport({ command, args });
const upstreamClient = new UpstreamClient({
name: command,
transport,
onTransportError: (error) => {
import_node_process5.default.stderr.write(`Upstream MCP transport error: ${error.message}
`);
shutdown(1);
}
const transport = new import_stdio3.StdioClientTransport({ command, args });
const mcps = /* @__PURE__ */ new Map([[SINGLE_MCP_NAME, { transport }]]);
const orchestrator = buildOrchestrator({
mcps,
namespaced: false,
transportErrorPrefix: () => "Upstream MCP"
});
const shutdown = (exitCode) => {
if (isShuttingDown) return;
isShuttingDown = true;
upstreamClient.disconnect().catch((error) => {
import_node_process5.default.stderr.write(
`dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
`
);
}).finally(() => import_node_process5.default.exit(exitCode));
};
try {
await upstreamClient.connect();
} catch (error) {
import_node_process5.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
`);
import_node_process5.default.exit(1);
}
let tools;
try {
tools = await upstreamClient.listTools();
} catch (error) {
import_node_process5.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
`);
import_node_process5.default.exit(1);
}
const catalog = ToolCatalog.fromFlat(tools);
const proxyServer = new ProxyServer({
catalog,
callTool: (name, input) => upstreamClient.callTool(name, input)
});
import_node_process5.default.on("SIGINT", () => shutdown(0));
import_node_process5.default.on("SIGTERM", () => shutdown(0));
import_node_process5.default.stdin.on("end", () => shutdown(0));
import_node_process5.default.stdin.on("close", () => shutdown(0));
try {
await proxyServer.start();
} catch (error) {
shutdown(1);
throw error;
}
await runProxy(orchestrator);
}
async function startProxyFromConfig(options = {}) {
let isShuttingDown = false;
const config = loadConfig(options);

@@ -757,10 +1612,25 @@ const mcps = /* @__PURE__ */ new Map();

}
const orchestrator = new Orchestrator({
const orchestrator = buildOrchestrator({
mcps,
namespaced: true,
transportErrorPrefix: (mcpName2) => `Upstream MCP "${mcpName2}"`
});
await runProxy(orchestrator);
}
var activeShutdown = { shutdown: null };
function buildOrchestrator(params) {
return new Orchestrator({
mcps: params.mcps,
namespaced: params.namespaced,
onTransportError: (mcpName2, error) => {
import_node_process5.default.stderr.write(`Upstream MCP "${mcpName2}" transport error: ${error.message}
`);
shutdown(1);
import_node_process6.default.stderr.write(
`${params.transportErrorPrefix(mcpName2)} transport error: ${error.message}
`
);
activeShutdown.shutdown?.(1);
}
});
}
async function runProxy(orchestrator) {
let isShuttingDown = false;
const shutdown = (exitCode) => {

@@ -770,23 +1640,52 @@ if (isShuttingDown) return;

orchestrator.disconnectAll().catch((error) => {
import_node_process5.default.stderr.write(
import_node_process6.default.stderr.write(
`dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
`
);
}).finally(() => import_node_process5.default.exit(exitCode));
}).finally(() => import_node_process6.default.exit(exitCode));
};
activeShutdown.shutdown = shutdown;
try {
await orchestrator.connect();
} catch (error) {
import_node_process5.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
import_node_process6.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
`);
import_node_process5.default.exit(1);
import_node_process6.default.exit(1);
return;
}
const proxyServer = new ProxyServer({
catalog: orchestrator.catalog,
callTool: (name, input) => orchestrator.callTool(name, input)
catalog: () => orchestrator.catalog,
capabilities: orchestrator.capabilities,
callTool: (name, input, options) => orchestrator.callTool(name, input, options),
resources: orchestrator.capabilities.resources !== void 0 ? {
listResources: () => orchestrator.listResources(),
listResourceTemplates: () => orchestrator.listResourceTemplates(),
readResource: (uri, options) => orchestrator.readResource(uri, options),
subscribeResource: (uri, options) => orchestrator.subscribeResource(uri, options),
unsubscribeResource: (uri, options) => orchestrator.unsubscribeResource(uri, options)
} : void 0,
prompts: orchestrator.capabilities.prompts !== void 0 ? {
listPrompts: () => orchestrator.listPrompts(),
getPrompt: (name, args, options) => orchestrator.getPrompt(name, args, options)
} : void 0,
complete: orchestrator.capabilities.completions !== void 0 ? (params, options) => orchestrator.complete(params, options) : void 0,
setLoggingLevel: orchestrator.capabilities.logging !== void 0 ? (level, options) => orchestrator.setLoggingLevel(level, options) : void 0,
onRootsListChanged: () => orchestrator.broadcastRootsListChanged()
});
import_node_process5.default.on("SIGINT", () => shutdown(0));
import_node_process5.default.on("SIGTERM", () => shutdown(0));
import_node_process5.default.stdin.on("end", () => shutdown(0));
import_node_process5.default.stdin.on("close", () => shutdown(0));
orchestrator.setNotificationHandlers({
onToolsListChanged: () => proxyServer.sendToolListChanged(),
onResourcesListChanged: () => proxyServer.sendResourceListChanged(),
onResourceUpdated: (params) => proxyServer.sendResourceUpdated(params),
onPromptsListChanged: () => proxyServer.sendPromptListChanged(),
onLogMessage: (params) => proxyServer.sendLoggingMessage(params)
});
orchestrator.setServerRequestForwarders({
onCreateMessage: (params, options) => proxyServer.forwardCreateMessage(params, options),
onElicitInput: (params, options) => proxyServer.forwardElicitInput(params, options),
onListRoots: (params, options) => proxyServer.forwardListRoots(params, options)
});
import_node_process6.default.on("SIGINT", () => shutdown(0));
import_node_process6.default.on("SIGTERM", () => shutdown(0));
import_node_process6.default.stdin.on("end", () => shutdown(0));
import_node_process6.default.stdin.on("close", () => shutdown(0));
try {

@@ -811,16 +1710,13 @@ await proxyServer.start();

"\nExamples:\n dynmcp -- npx -y chrome-devtools-mcp@latest\n dynmcp --config ./mcp.json\n"
).option("-c, --config <path>", "Path to config file (JSON or YAML)").option(
"-e, --env <path>",
"Path to a .env file for environment variable interpolation"
).allowExcessArguments(true).passThroughOptions(true).action(async (_options, cmd) => {
const separatorIndex = import_node_process6.default.argv.indexOf("--");
).option("-c, --config <path>", "Path to config file (JSON or YAML)").option("-e, --env <path>", "Path to a .env file for environment variable interpolation").allowExcessArguments(true).passThroughOptions(true).action(async (_options, cmd) => {
const separatorIndex = import_node_process7.default.argv.indexOf("--");
const configPath = cmd.opts().config;
const envFilePath = cmd.opts().env;
if (separatorIndex !== -1) {
const [command, ...args] = import_node_process6.default.argv.slice(separatorIndex + 1);
const [command, ...args] = import_node_process7.default.argv.slice(separatorIndex + 1);
if (command === void 0) {
import_node_process6.default.stderr.write(
import_node_process7.default.stderr.write(
"dynmcp: no upstream command provided after --.\nUsage: dynmcp -- <command> [args...]\n"
);
import_node_process6.default.exit(1);
import_node_process7.default.exit(1);
}

@@ -830,5 +1726,5 @@ try {

} catch (error) {
import_node_process6.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
import_node_process7.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
`);
import_node_process6.default.exit(1);
import_node_process7.default.exit(1);
}

@@ -840,5 +1736,5 @@ return;

} catch (error) {
import_node_process6.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
import_node_process7.default.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
`);
import_node_process6.default.exit(1);
import_node_process7.default.exit(1);
}

@@ -848,7 +1744,7 @@ });

// src/index.ts
var import_node_process7 = __toESM(require("process"), 1);
var import_node_process8 = __toESM(require("process"), 1);
async function main() {
cli.parse(import_node_process7.default.argv);
cli.parse(import_node_process8.default.argv);
}
main();
//# sourceMappingURL=index.cjs.map
+1097
-179
#!/usr/bin/env node
// src/cli.ts
import process6 from "process";
import process7 from "process";
import { Command } from "commander";

@@ -10,3 +10,3 @@

name: "dynmcp",
version: "0.2.0",
version: "0.3.0",
description: "Dynamic MCP context management tool for AI MCP-enabled agents and clients.",

@@ -91,2 +91,3 @@ author: "Brandon Burrus <brandon@burrus.io>",

"@types/node": "^25.9.0",
"@vitest/coverage-v8": "^4.1.6",
husky: "^9.1.7",

@@ -105,3 +106,3 @@ tsup: "^8.5.1",

// src/proxy/index.ts
import process5 from "process";
import process6 from "process";
import { StdioClientTransport as StdioClientTransport2 } from "@modelcontextprotocol/sdk/client/stdio.js";

@@ -369,29 +370,35 @@

// src/proxy/transport-factory.ts
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
function createTransport(config) {
switch (config.transport) {
case "stdio":
return new StdioClientTransport({
command: config.command,
args: config.args,
env: config.env
});
case "streamable-http":
return new StreamableHTTPClientTransport(
new URL(config.url),
config.headers ? { requestInit: { headers: config.headers } } : void 0
);
case "sse":
return new SSEClientTransport(
new URL(config.url),
config.headers ? { requestInit: { headers: config.headers } } : void 0
);
default: {
const _exhaustive = config;
return _exhaustive;
// src/proxy/orchestrator.ts
import process4 from "process";
// src/proxy/capability-aggregator.ts
function aggregateCapabilities(upstreams) {
const aggregated = {
tools: { listChanged: true }
};
for (const caps of upstreams) {
if (caps === void 0) continue;
if (caps.resources !== void 0) {
aggregated.resources ??= {};
if (caps.resources.subscribe === true) {
aggregated.resources.subscribe = true;
}
if (caps.resources.listChanged === true) {
aggregated.resources.listChanged = true;
}
}
if (caps.prompts !== void 0) {
aggregated.prompts ??= {};
if (caps.prompts.listChanged === true) {
aggregated.prompts.listChanged = true;
}
}
if (caps.logging !== void 0) {
aggregated.logging ??= {};
}
if (caps.completions !== void 0) {
aggregated.completions ??= {};
}
}
return aggregated;
}

@@ -502,11 +509,256 @@

// src/proxy/notification-forwarder.ts
var NotificationForwarder = class {
constructor(registry, resourceRouter, promptRouter, toolsByMcp, setToolCatalog, namespaced) {
this.registry = registry;
this.resourceRouter = resourceRouter;
this.promptRouter = promptRouter;
this.toolsByMcp = toolsByMcp;
this.setToolCatalog = setToolCatalog;
this.namespaced = namespaced;
}
registry;
resourceRouter;
promptRouter;
toolsByMcp;
setToolCatalog;
namespaced;
hostHandlers = {};
setHostHandlers(handlers) {
this.hostHandlers = handlers;
}
async handleToolsListChanged(mcpName2) {
const client = this.registry.get(mcpName2);
if (client === void 0) return;
const tools = await client.listTools().catch(() => []);
this.toolsByMcp.set(mcpName2, tools);
const rebuilt = this.namespaced ? ToolCatalog.fromGrouped(this.toolsByMcp) : ToolCatalog.fromFlat([...this.toolsByMcp.values()][0] ?? []);
this.setToolCatalog(rebuilt);
await this.hostHandlers.onToolsListChanged?.();
}
async handleResourcesListChanged(mcpName2) {
const router = this.resourceRouter();
const client = this.registry.get(mcpName2);
if (router === null || client === void 0) return;
const [resources, templates] = await Promise.all([
client.listResources().catch(() => []),
client.listResourceTemplates().catch(() => [])
]);
router.setResources(mcpName2, resources);
router.setTemplates(mcpName2, templates);
await this.hostHandlers.onResourcesListChanged?.();
}
async handleResourceUpdated(params) {
await this.hostHandlers.onResourceUpdated?.(params);
}
async handlePromptsListChanged(mcpName2) {
const router = this.promptRouter();
const client = this.registry.get(mcpName2);
if (router === null || client === void 0) return;
const prompts = await client.listPrompts().catch(() => []);
router.setPrompts(mcpName2, prompts);
await this.hostHandlers.onPromptsListChanged?.();
}
/**
* Rewrites the upstream's `logger` field with the originating MCP's name as a
* prefix so the host can attribute log lines, then forwards the message to the
* host's log message handler.
*/
async handleLogMessage(mcpName2, params) {
const handler = this.hostHandlers.onLogMessage;
if (handler === void 0) return;
if (!this.namespaced) {
await handler(params);
return;
}
const prefixed = {
...params,
logger: params.logger === void 0 ? mcpName2 : `${mcpName2}/${params.logger}`
};
await handler(prefixed);
}
};
// src/proxy/prompt-router.ts
var PromptRouter = class {
mcpOrder;
perMcp;
nameOwners = /* @__PURE__ */ new Map();
detectedCollisions = [];
constructor(mcpOrder) {
this.mcpOrder = [...mcpOrder];
this.perMcp = new Map(this.mcpOrder.map((name) => [name, []]));
}
setPrompts(mcpName2, prompts) {
const entry = this.perMcp.get(mcpName2);
if (entry === void 0) {
throw new Error(`PromptRouter: unknown mcp "${mcpName2}"`);
}
this.perMcp.set(mcpName2, [...prompts]);
this.rebuild();
}
aggregatedPrompts() {
const result = [];
for (const mcpName2 of this.mcpOrder) {
const entry = this.perMcp.get(mcpName2);
if (entry !== void 0) {
result.push(...entry);
}
}
return result;
}
ownerOf(promptName) {
return this.nameOwners.get(promptName);
}
collisions() {
return this.detectedCollisions;
}
rebuild() {
this.nameOwners = /* @__PURE__ */ new Map();
const collisions = [];
for (const mcpName2 of this.mcpOrder) {
const prompts = this.perMcp.get(mcpName2);
if (prompts === void 0) continue;
for (const prompt of prompts) {
const existing = this.nameOwners.get(prompt.name);
if (existing === void 0) {
this.nameOwners.set(prompt.name, mcpName2);
} else {
collisions.push({ name: prompt.name, chosen: existing, shadowed: mcpName2 });
}
}
}
this.detectedCollisions = collisions;
}
};
// src/proxy/resource-router.ts
var ResourceRouter = class {
mcpOrder;
perMcp;
uriOwners = /* @__PURE__ */ new Map();
templateOwners = [];
detectedCollisions = [];
constructor(mcpOrder) {
this.mcpOrder = [...mcpOrder];
this.perMcp = new Map(
this.mcpOrder.map((name) => [name, { resources: [], templates: [] }])
);
}
setResources(mcpName2, resources) {
const entry = this.perMcp.get(mcpName2);
if (entry === void 0) {
throw new Error(`ResourceRouter: unknown mcp "${mcpName2}"`);
}
entry.resources = [...resources];
this.rebuild();
}
setTemplates(mcpName2, templates) {
const entry = this.perMcp.get(mcpName2);
if (entry === void 0) {
throw new Error(`ResourceRouter: unknown mcp "${mcpName2}"`);
}
entry.templates = [...templates];
this.rebuild();
}
aggregatedResources() {
const result = [];
for (const mcpName2 of this.mcpOrder) {
const entry = this.perMcp.get(mcpName2);
if (entry !== void 0) {
result.push(...entry.resources);
}
}
return result;
}
aggregatedTemplates() {
const result = [];
for (const mcpName2 of this.mcpOrder) {
const entry = this.perMcp.get(mcpName2);
if (entry !== void 0) {
result.push(...entry.templates);
}
}
return result;
}
/**
* Returns the mcpName that owns the given URI, or undefined if no upstream advertises it.
* Concrete URI matches take precedence over template prefix matches; templates are tried
* in config-file order (first-wins).
*/
ownerOf(uri) {
const concrete = this.uriOwners.get(uri);
if (concrete !== void 0) {
return concrete;
}
for (const { prefix, mcpName: mcpName2 } of this.templateOwners) {
if (prefix.length > 0 && uri.startsWith(prefix)) {
return mcpName2;
}
}
return void 0;
}
collisions() {
return this.detectedCollisions;
}
rebuild() {
this.uriOwners = /* @__PURE__ */ new Map();
this.templateOwners = [];
const collisions = [];
for (const mcpName2 of this.mcpOrder) {
const entry = this.perMcp.get(mcpName2);
if (entry === void 0) continue;
for (const resource of entry.resources) {
const existing = this.uriOwners.get(resource.uri);
if (existing === void 0) {
this.uriOwners.set(resource.uri, mcpName2);
} else {
collisions.push({ uri: resource.uri, chosen: existing, shadowed: mcpName2 });
}
}
for (const template of entry.templates) {
this.templateOwners.push({
prefix: literalPrefixOf(template.uriTemplate),
template,
mcpName: mcpName2
});
}
}
this.detectedCollisions = collisions;
}
};
function literalPrefixOf(uriTemplate) {
const idx = uriTemplate.indexOf("{");
return idx === -1 ? uriTemplate : uriTemplate.slice(0, idx);
}
// src/proxy/upstream-client.ts
import process3 from "process";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import {
CreateMessageRequestSchema,
ElicitRequestSchema,
ListRootsRequestSchema,
LoggingMessageNotificationSchema,
PromptListChangedNotificationSchema,
ResourceListChangedNotificationSchema,
ResourceUpdatedNotificationSchema,
ToolListChangedNotificationSchema
} from "@modelcontextprotocol/sdk/types.js";
var UpstreamClient = class {
transport;
onTransportError;
notificationHandlers;
serverRequestHandlers;
client = null;
constructor({ name, transport, onTransportError }) {
constructor({
name,
transport,
onTransportError,
notifications,
serverRequests
}) {
this.transport = transport;
this.notificationHandlers = notifications ?? {};
this.serverRequestHandlers = serverRequests ?? {};
this.onTransportError = onTransportError ?? ((error) => {

@@ -519,10 +771,76 @@ process3.stderr.write(`[${name}] Upstream MCP transport error: ${error.message}

this.transport.onerror = this.onTransportError;
this.client = new Client({ name: "dynamic-discovery-mcp", version: "1.0.0" });
this.client = new Client(
{ name: "dynamic-discovery-mcp", version: "1.0.0" },
{
capabilities: {
// Declare every client-side capability the proxy may relay on behalf of the host.
// Actual reachability of each feature depends on what the host supports — if the
// host does not support sampling, for instance, the host call returns an error
// which we forward back to the upstream verbatim.
sampling: {},
elicitation: {},
roots: { listChanged: true }
}
}
);
this.registerServerRequestHandlers(this.client);
if (this.notificationHandlers.onToolsListChanged !== void 0) {
this.client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
await this.notificationHandlers.onToolsListChanged?.();
});
}
if (this.notificationHandlers.onResourcesListChanged !== void 0) {
this.client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => {
await this.notificationHandlers.onResourcesListChanged?.();
});
}
if (this.notificationHandlers.onResourceUpdated !== void 0) {
this.client.setNotificationHandler(ResourceUpdatedNotificationSchema, async (notification) => {
await this.notificationHandlers.onResourceUpdated?.({ uri: notification.params.uri });
});
}
if (this.notificationHandlers.onPromptsListChanged !== void 0) {
this.client.setNotificationHandler(PromptListChangedNotificationSchema, async () => {
await this.notificationHandlers.onPromptsListChanged?.();
});
}
if (this.notificationHandlers.onLogMessage !== void 0) {
this.client.setNotificationHandler(LoggingMessageNotificationSchema, async (notification) => {
await this.notificationHandlers.onLogMessage?.(notification.params);
});
}
await this.client.connect(this.transport);
}
async listTools() {
if (this.client === null) {
throw new Error("Client is not connected. Call connect() first.");
async setLoggingLevel(level, options) {
const client = this.requireClient();
await client.setLoggingLevel(level, options);
}
async listPrompts(options) {
const client = this.requireClient();
const result = await client.listPrompts(void 0, options);
return result.prompts;
}
async getPrompt(name, args, options) {
const client = this.requireClient();
const params = { name };
if (args !== void 0) {
params.arguments = args;
}
const result = await this.client.listTools();
return client.getPrompt(params, options);
}
async complete(params, options) {
const client = this.requireClient();
return client.complete(params, options);
}
/**
* Returns the capabilities advertised by the upstream server during initialize.
* Returns `undefined` if the client is not connected, or if the SDK has not yet
* recorded the server's capabilities (e.g. during a partially-completed handshake).
*/
getCapabilities() {
return this.client?.getServerCapabilities();
}
async listTools(options) {
const client = this.requireClient();
const result = await client.listTools(void 0, options);
return result.tools.map((tool) => {

@@ -549,9 +867,29 @@ const upstreamTool = {

}
async callTool(name, input) {
if (this.client === null) {
throw new Error("Client is not connected. Call connect() first.");
}
const result = await this.client.callTool({ name, arguments: input });
async callTool(name, input, options) {
const client = this.requireClient();
const result = await client.callTool({ name, arguments: input }, void 0, options);
return result;
}
async listResources(options) {
const client = this.requireClient();
const result = await client.listResources(void 0, options);
return result.resources;
}
async listResourceTemplates(options) {
const client = this.requireClient();
const result = await client.listResourceTemplates(void 0, options);
return result.resourceTemplates;
}
async readResource(uri, options) {
const client = this.requireClient();
return client.readResource({ uri }, options);
}
async subscribeResource(uri, options) {
const client = this.requireClient();
await client.subscribeResource({ uri }, options);
}
async unsubscribeResource(uri, options) {
const client = this.requireClient();
await client.unsubscribeResource({ uri }, options);
}
async disconnect() {

@@ -564,27 +902,65 @@ if (this.client === null) {

}
/**
* Sends `notifications/roots/list_changed` to the upstream, letting it know that
* the host's set of filesystem roots has changed.
*/
async sendRootsListChanged() {
const client = this.requireClient();
await client.sendRootsListChanged();
}
registerServerRequestHandlers(client) {
if (this.serverRequestHandlers.onCreateMessage !== void 0) {
client.setRequestHandler(
CreateMessageRequestSchema,
async (request, extra) => {
return this.serverRequestHandlers.onCreateMessage(request.params, {
signal: extra.signal
});
}
);
}
if (this.serverRequestHandlers.onElicitInput !== void 0) {
client.setRequestHandler(
ElicitRequestSchema,
async (request, extra) => {
return this.serverRequestHandlers.onElicitInput(request.params, {
signal: extra.signal
});
}
);
}
if (this.serverRequestHandlers.onListRoots !== void 0) {
client.setRequestHandler(
ListRootsRequestSchema,
async (request, extra) => {
return this.serverRequestHandlers.onListRoots(request.params, {
signal: extra.signal
});
}
);
}
}
requireClient() {
if (this.client === null) {
throw new Error("Client is not connected. Call connect() first.");
}
return this.client;
}
};
// src/proxy/orchestrator.ts
var Orchestrator = class {
config;
// src/proxy/upstream-registry.ts
var UpstreamRegistry = class {
clients = /* @__PURE__ */ new Map();
toolCatalog = null;
constructor(config) {
this.config = config;
}
async connect() {
const groups = /* @__PURE__ */ new Map();
async connectAll(entries) {
try {
for (const [mcpName2, { transport }] of this.config.mcps) {
for (const [mcpName2, config] of entries) {
const client = new UpstreamClient({
name: mcpName2,
transport,
onTransportError: (error) => {
this.config.onTransportError?.(mcpName2, error);
}
transport: config.transport,
onTransportError: config.onTransportError,
notifications: config.notifications,
serverRequests: config.serverRequests
});
await client.connect();
const tools = await client.listTools();
this.clients.set(mcpName2, client);
groups.set(mcpName2, tools);
}

@@ -595,4 +971,127 @@ } catch (error) {

}
this.toolCatalog = ToolCatalog.fromGrouped(groups);
}
get(mcpName2) {
return this.clients.get(mcpName2);
}
/**
* Returns the sole connected client. Used by single-MCP (`--`) mode where the
* Orchestrator guarantees there is exactly one upstream. Returns undefined when
* zero or more than one client is connected.
*/
sole() {
if (this.clients.size !== 1) return void 0;
return this.clients.values().next().value;
}
names() {
return [...this.clients.keys()];
}
entries() {
return this.clients.entries();
}
size() {
return this.clients.size;
}
async disconnectAll() {
const disconnections = [...this.clients.values()].map((client) => client.disconnect());
await Promise.all(disconnections);
this.clients.clear();
}
};
// src/proxy/orchestrator.ts
var Orchestrator = class {
config;
registry = new UpstreamRegistry();
toolsByMcp = /* @__PURE__ */ new Map();
resourceRouter = null;
promptRouter = null;
toolCatalog = null;
aggregatedCapabilities = null;
serverRequestForwarders = {};
forwarder;
constructor(config) {
if (!config.namespaced && config.mcps.size !== 1) {
throw new Error(
`Single-MCP (non-namespaced) mode requires exactly one upstream; got ${config.mcps.size}.`
);
}
this.config = config;
this.forwarder = new NotificationForwarder(
this.registry,
() => this.resourceRouter,
() => this.promptRouter,
this.toolsByMcp,
(catalog) => {
this.toolCatalog = catalog;
},
this.config.namespaced
);
}
setNotificationHandlers(handlers) {
this.forwarder.setHostHandlers(handlers);
}
setServerRequestForwarders(forwarders) {
this.serverRequestForwarders = forwarders;
}
async connect() {
const resourceRouter = new ResourceRouter([...this.config.mcps.keys()]);
const promptRouter = new PromptRouter([...this.config.mcps.keys()]);
const upstreamEntries = [
...this.config.mcps
].map(([mcpName2, { transport }]) => [
mcpName2,
{
transport,
onTransportError: (error) => {
this.config.onTransportError?.(mcpName2, error);
},
notifications: {
onToolsListChanged: () => this.forwarder.handleToolsListChanged(mcpName2),
onResourcesListChanged: () => this.forwarder.handleResourcesListChanged(mcpName2),
onResourceUpdated: (params) => this.forwarder.handleResourceUpdated(params),
onPromptsListChanged: () => this.forwarder.handlePromptsListChanged(mcpName2),
onLogMessage: (params) => this.forwarder.handleLogMessage(mcpName2, params)
},
serverRequests: {
onCreateMessage: (params, opts) => this.forwardCreateMessage(params, opts),
onElicitInput: (params, opts) => this.forwardElicitInput(params, opts),
onListRoots: (params, opts) => this.forwardListRoots(params, opts)
}
}
]);
await this.registry.connectAll(upstreamEntries);
const capabilityList = [];
this.toolsByMcp.clear();
for (const [mcpName2, client] of this.registry.entries()) {
const caps = client.getCapabilities();
capabilityList.push(caps);
const tools = await client.listTools();
this.toolsByMcp.set(mcpName2, tools);
if (caps?.resources !== void 0) {
const [resources, templates] = await Promise.all([
client.listResources().catch(() => []),
client.listResourceTemplates().catch(() => [])
]);
resourceRouter.setResources(mcpName2, resources);
resourceRouter.setTemplates(mcpName2, templates);
}
if (caps?.prompts !== void 0) {
const prompts = await client.listPrompts().catch(() => []);
promptRouter.setPrompts(mcpName2, prompts);
}
}
this.toolCatalog = this.config.namespaced ? ToolCatalog.fromGrouped(this.toolsByMcp) : ToolCatalog.fromFlat([...this.toolsByMcp.values()][0] ?? []);
this.aggregatedCapabilities = aggregateCapabilities(capabilityList);
this.resourceRouter = resourceRouter;
this.promptRouter = promptRouter;
logCollisions(resourceRouter, promptRouter);
}
async disconnectAll() {
await this.registry.disconnectAll();
this.toolsByMcp.clear();
this.toolCatalog = null;
this.aggregatedCapabilities = null;
this.resourceRouter = null;
this.promptRouter = null;
}
get catalog() {

@@ -604,126 +1103,504 @@ if (this.toolCatalog === null) {

}
async callTool(namespacedName, input) {
const separatorIndex = namespacedName.indexOf("/");
if (separatorIndex === -1) {
throw new Error(
`Invalid namespaced tool name: "${namespacedName}". Expected format: "mcpName/toolName".`
);
get capabilities() {
if (this.aggregatedCapabilities === null) {
throw new Error("Orchestrator is not connected. Call connect() first.");
}
const mcpName2 = namespacedName.slice(0, separatorIndex);
const toolName = namespacedName.slice(separatorIndex + 1);
const client = this.clients.get(mcpName2);
return this.aggregatedCapabilities;
}
// === Forward-direction request routing ===
async callTool(displayName, input, options) {
if (this.config.namespaced) {
const { mcpName: mcpName2, toolName } = splitNamespacedName(displayName, this.registry.names());
return this.requireClient(mcpName2, "tool").callTool(toolName, input, options);
}
const sole = this.registry.sole();
if (sole === void 0) {
throw new Error("Orchestrator is not connected. Call connect() first.");
}
return sole.callTool(displayName, input, options);
}
listResources() {
return this.requireResourceRouter().aggregatedResources();
}
listResourceTemplates() {
return this.requireResourceRouter().aggregatedTemplates();
}
async readResource(uri, options) {
return this.resolveResourceOwner(uri).readResource(uri, options);
}
async subscribeResource(uri, options) {
await this.resolveResourceOwner(uri).subscribeResource(uri, options);
}
async unsubscribeResource(uri, options) {
await this.resolveResourceOwner(uri).unsubscribeResource(uri, options);
}
listPrompts() {
return this.requirePromptRouter().aggregatedPrompts();
}
async getPrompt(name, args, options) {
return this.resolvePromptOwner(name).getPrompt(name, args, options);
}
async complete(params, options) {
const client = this.resolveCompletionTarget(params.ref);
return client.complete(params, options);
}
// === Broadcasts ===
/**
* Broadcasts a `logging/setLevel` request to every upstream advertising the logging
* capability. Errors from individual upstreams are swallowed so a single misbehaving
* upstream cannot break the broadcast for others; failures are written to stderr.
*/
async setLoggingLevel(level, options) {
await this.broadcastAsync(
(client) => client.getCapabilities()?.logging !== void 0 ? client.setLoggingLevel(level, options) : Promise.resolve(),
"setLoggingLevel"
);
}
/**
* Broadcasts `notifications/roots/list_changed` to every connected upstream — the
* proxy declares roots capability uniformly to all upstreams so every one of them
* may have requested roots and needs to know the list changed.
*/
async broadcastRootsListChanged() {
await this.broadcastAsync((client) => client.sendRootsListChanged(), "sendRootsListChanged");
}
// === Server-initiated request forwarders (upstream → host) ===
async forwardCreateMessage(params, options) {
const handler = this.serverRequestForwarders.onCreateMessage;
if (handler === void 0) {
throw new Error("Proxy does not support sampling: host has not registered a handler.");
}
return handler(params, options);
}
async forwardElicitInput(params, options) {
const handler = this.serverRequestForwarders.onElicitInput;
if (handler === void 0) {
throw new Error("Proxy does not support elicitation: host has not registered a handler.");
}
return handler(params, options);
}
async forwardListRoots(params, options) {
const handler = this.serverRequestForwarders.onListRoots;
if (handler === void 0) {
throw new Error("Proxy does not support roots: host has not registered a handler.");
}
return handler(params, options);
}
// === Internal helpers ===
requireResourceRouter() {
if (this.resourceRouter === null) {
throw new Error("Orchestrator is not connected. Call connect() first.");
}
return this.resourceRouter;
}
requirePromptRouter() {
if (this.promptRouter === null) {
throw new Error("Orchestrator is not connected. Call connect() first.");
}
return this.promptRouter;
}
resolveResourceOwner(uri) {
const owner = this.requireResourceRouter().ownerOf(uri);
if (owner === void 0) {
throw new Error(`Unknown resource URI: "${uri}". No upstream MCP advertises it.`);
}
return this.requireClient(owner, "resource");
}
resolvePromptOwner(name) {
const owner = this.requirePromptRouter().ownerOf(name);
if (owner === void 0) {
throw new Error(`Unknown prompt: "${name}". No upstream MCP advertises it.`);
}
return this.requireClient(owner, "prompt");
}
resolveCompletionTarget(ref) {
if (ref.type === "ref/prompt") {
return this.resolvePromptOwner(ref.name);
}
if (ref.type === "ref/resource") {
return this.resolveResourceOwner(ref.uri);
}
const unknownRef = ref;
throw new Error(`Unsupported completion ref type: "${unknownRef.type}"`);
}
requireClient(mcpName2, role) {
const client = this.registry.get(mcpName2);
if (client === void 0) {
const available = [...this.clients.keys()].sort().join(", ");
throw new Error(`Unknown MCP: "${mcpName2}". Available MCPs: ${available}`);
throw new Error(`Internal error: ${role} owner "${mcpName2}" has no connected client.`);
}
return client.callTool(toolName, input);
return client;
}
async disconnectAll() {
const disconnections = [...this.clients.values()].map((client) => client.disconnect());
await Promise.all(disconnections);
this.clients.clear();
this.toolCatalog = null;
async broadcastAsync(action, label) {
const targets = [];
for (const [mcpName2, client] of this.registry.entries()) {
targets.push(
action(client).catch((error) => {
const message = error instanceof Error ? error.message : String(error);
process4.stderr.write(`dynmcp: ${label} failed for "${mcpName2}": ${message}
`);
})
);
}
await Promise.all(targets);
}
};
function splitNamespacedName(namespacedName, knownMcpNames) {
const separatorIndex = namespacedName.indexOf("/");
if (separatorIndex === -1) {
throw new Error(
`Invalid namespaced tool name: "${namespacedName}". Expected format: "mcpName/toolName".`
);
}
const mcpName2 = namespacedName.slice(0, separatorIndex);
const toolName = namespacedName.slice(separatorIndex + 1);
if (!knownMcpNames.includes(mcpName2)) {
const available = [...knownMcpNames].sort().join(", ");
throw new Error(`Unknown MCP: "${mcpName2}". Available MCPs: ${available}`);
}
return { mcpName: mcpName2, toolName };
}
function logCollisions(resourceRouter, promptRouter) {
for (const collision of resourceRouter.collisions()) {
process4.stderr.write(
`dynmcp: resource URI collision: "${collision.uri}" is provided by "${collision.chosen}" and "${collision.shadowed}"; routing to "${collision.chosen}".
`
);
}
for (const collision of promptRouter.collisions()) {
process4.stderr.write(
`dynmcp: prompt name collision: "${collision.name}" is provided by "${collision.chosen}" and "${collision.shadowed}"; routing to "${collision.chosen}".
`
);
}
}
// src/proxy/server.ts
import process4 from "process";
import { FastMCP } from "fastmcp";
import process5 from "process";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
CompleteRequestSchema,
GetPromptRequestSchema,
ListPromptsRequestSchema,
ListResourcesRequestSchema,
ListResourceTemplatesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
RootsListChangedNotificationSchema,
SetLevelRequestSchema,
SubscribeRequestSchema,
UnsubscribeRequestSchema
} from "@modelcontextprotocol/sdk/types.js";
import { z as z3 } from "zod";
var DISCOVER_TOOL_NAME = "discover_tool";
var USE_TOOL_NAME = "use_tool";
var USE_TOOL_DESCRIPTION = "Use a tool that was previously discovered with the discover_tool tool.";
var DISCOVER_TOOL_INPUT_SCHEMA = {
type: "object",
properties: {
tool_name: { type: "string" }
},
required: ["tool_name"]
};
var USE_TOOL_INPUT_SCHEMA = {
type: "object",
properties: {
tool_name: { type: "string" },
tool_input: { type: "object", additionalProperties: true, default: {} }
},
required: ["tool_name"]
};
var DiscoverToolArgsSchema = z3.object({ tool_name: z3.string() });
var UseToolArgsSchema = z3.object({
tool_name: z3.string(),
tool_input: z3.record(z3.string(), z3.unknown()).default({})
});
var ProxyServer = class {
catalog;
callTool;
constructor({ catalog, callTool }) {
capabilities;
resources;
prompts;
complete;
setLoggingLevelCallback;
onRootsListChangedCallback;
sdkServer = null;
constructor({
catalog,
callTool,
capabilities,
resources,
prompts,
complete,
setLoggingLevel,
onRootsListChanged
}) {
this.catalog = catalog;
this.callTool = callTool;
this.capabilities = capabilities;
this.resources = resources;
this.prompts = prompts;
this.complete = complete;
this.setLoggingLevelCallback = setLoggingLevel;
this.onRootsListChangedCallback = onRootsListChanged;
}
buildServer() {
const server = new Server(
{
name: "dynamic-discovery-mcp",
version: package_default.version
},
{
capabilities: this.capabilities
}
);
this.registerToolHandlers(server);
if (this.capabilities.resources !== void 0 && this.resources !== void 0) {
this.registerResourceHandlers(server, this.resources);
}
if (this.capabilities.prompts !== void 0 && this.prompts !== void 0) {
this.registerPromptHandlers(server, this.prompts);
}
if (this.capabilities.completions !== void 0 && this.complete !== void 0) {
this.registerCompletionHandler(server, this.complete);
}
if (this.capabilities.logging !== void 0 && this.setLoggingLevelCallback !== void 0) {
this.registerLoggingHandler(server, this.setLoggingLevelCallback);
}
if (this.onRootsListChangedCallback !== void 0) {
const callback = this.onRootsListChangedCallback;
server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
await callback();
});
}
this.sdkServer = server;
return server;
}
/**
* Forwards an upstream-initiated `sampling/createMessage` request to the host. The
* upstream's abort signal is threaded through so cancellation by the upstream
* propagates to the host.
*/
async forwardCreateMessage(params, options) {
const server = this.requireSdkServer();
return server.createMessage(params, options);
}
/**
* Forwards an upstream-initiated `elicitation/create` request to the host.
*/
async forwardElicitInput(params, options) {
const server = this.requireSdkServer();
return server.elicitInput(params, options);
}
/**
* Forwards an upstream-initiated `roots/list` request to the host.
*/
async forwardListRoots(params, options) {
const server = this.requireSdkServer();
return server.listRoots(params, options);
}
requireSdkServer() {
if (this.sdkServer === null) {
throw new Error("ProxyServer is not built. Call buildServer() before forwarding requests.");
}
return this.sdkServer;
}
async start() {
const server = new FastMCP({
name: "dynamic-discovery-mcp",
version: package_default.version
});
server.addTool({
name: "discover_tool",
description: this.catalog.discoverToolDescription,
parameters: z3.object({ tool_name: z3.string() }),
execute: async ({ tool_name }) => {
return this.catalog.getToolDetails(tool_name);
const server = this.buildServer();
const transport = new StdioServerTransport();
process5.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
await server.connect(transport);
}
/**
* Notifies the host that the discover_tool description has changed because an upstream
* emitted `notifications/tools/list_changed`. The host should re-fetch the tools list
* to pick up the regenerated catalog. Silently no-ops if `buildServer()` has not been
* called yet.
*/
async sendToolListChanged() {
if (this.sdkServer !== null) {
await this.sdkServer.sendToolListChanged();
}
}
/**
* Notifies the host that the proxy's aggregated resource list has changed. Silently
* no-ops if `buildServer()` has not been called yet. Errors propagate.
*/
async sendResourceListChanged() {
if (this.sdkServer !== null) {
await this.sdkServer.sendResourceListChanged();
}
}
/**
* Notifies the host that a specific subscribed resource has changed. Silently no-ops
* if `buildServer()` has not been called yet.
*/
async sendResourceUpdated(params) {
if (this.sdkServer !== null) {
await this.sdkServer.sendResourceUpdated(params);
}
}
/**
* Notifies the host that the proxy's aggregated prompt list has changed. Silently
* no-ops if `buildServer()` has not been called yet.
*/
async sendPromptListChanged() {
if (this.sdkServer !== null) {
await this.sdkServer.sendPromptListChanged();
}
}
registerToolHandlers(server) {
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: DISCOVER_TOOL_NAME,
description: this.catalog().discoverToolDescription,
inputSchema: DISCOVER_TOOL_INPUT_SCHEMA
},
{
name: USE_TOOL_NAME,
description: USE_TOOL_DESCRIPTION,
inputSchema: USE_TOOL_INPUT_SCHEMA
}
]
}));
server.setRequestHandler(
CallToolRequestSchema,
async (request, extra) => {
const { name, arguments: rawArgs } = request.params;
const catalog = this.catalog();
if (name === DISCOVER_TOOL_NAME) {
const args = DiscoverToolArgsSchema.parse(rawArgs ?? {});
return {
content: [{ type: "text", text: catalog.getToolDetails(args.tool_name) }]
};
}
if (name === USE_TOOL_NAME) {
const args = UseToolArgsSchema.parse(rawArgs ?? {});
if (!catalog.tools.has(args.tool_name)) {
return {
content: [{ type: "text", text: catalog.getToolDetails(args.tool_name) }]
};
}
return await this.callTool(args.tool_name, args.tool_input, { signal: extra.signal });
}
return {
isError: true,
content: [{ type: "text", text: `Unknown tool: "${name}"` }]
};
}
);
}
registerResourceHandlers(server, callbacks) {
server.setRequestHandler(
ListResourcesRequestSchema,
async () => ({
resources: callbacks.listResources()
})
);
server.setRequestHandler(
ListResourceTemplatesRequestSchema,
async () => ({
resourceTemplates: callbacks.listResourceTemplates()
})
);
server.setRequestHandler(
ReadResourceRequestSchema,
async (request, extra) => {
return callbacks.readResource(request.params.uri, { signal: extra.signal });
}
);
server.setRequestHandler(SubscribeRequestSchema, async (request, extra) => {
await callbacks.subscribeResource(request.params.uri, { signal: extra.signal });
return {};
});
server.addTool({
name: "use_tool",
description: "Use a tool that was previously discovered with the discover_tool tool.",
parameters: z3.object({
tool_name: z3.string(),
tool_input: z3.record(z3.string(), z3.unknown()).default({})
}),
execute: async ({ tool_name, tool_input }) => {
if (!this.catalog.tools.has(tool_name)) {
return this.catalog.getToolDetails(tool_name);
}
const result = await this.callTool(tool_name, tool_input);
return result;
server.setRequestHandler(UnsubscribeRequestSchema, async (request, extra) => {
await callbacks.unsubscribeResource(request.params.uri, { signal: extra.signal });
return {};
});
}
registerPromptHandlers(server, callbacks) {
server.setRequestHandler(
ListPromptsRequestSchema,
async () => ({
prompts: callbacks.listPrompts()
})
);
server.setRequestHandler(
GetPromptRequestSchema,
async (request, extra) => {
return callbacks.getPrompt(request.params.name, request.params.arguments, {
signal: extra.signal
});
}
);
}
registerCompletionHandler(server, callback) {
server.setRequestHandler(
CompleteRequestSchema,
async (request, extra) => {
return callback(request.params, { signal: extra.signal });
}
);
}
registerLoggingHandler(server, callback) {
server.setRequestHandler(SetLevelRequestSchema, async (request, extra) => {
await callback(request.params.level, { signal: extra.signal });
return {};
});
process4.stderr.write("Starting dynamic-discovery-mcp server over stdio\n");
await server.start({ transportType: "stdio" });
}
/**
* Forwards a log message from an upstream MCP to the host. Silently no-ops if
* `buildServer()` has not been called yet.
*/
async sendLoggingMessage(params) {
if (this.sdkServer !== null) {
await this.sdkServer.sendLoggingMessage(params);
}
}
};
// src/proxy/transport-factory.ts
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
function createTransport(config) {
switch (config.transport) {
case "stdio":
return new StdioClientTransport({
command: config.command,
args: config.args,
env: config.env
});
case "streamable-http":
return new StreamableHTTPClientTransport(
new URL(config.url),
config.headers ? { requestInit: { headers: config.headers } } : void 0
);
case "sse":
return new SSEClientTransport(
new URL(config.url),
config.headers ? { requestInit: { headers: config.headers } } : void 0
);
default: {
const _exhaustive = config;
return _exhaustive;
}
}
}
// src/proxy/index.ts
var SINGLE_MCP_NAME = "__default__";
async function startProxy(command, args) {
let isShuttingDown = false;
const transport = new StdioClientTransport2({ command, args });
const upstreamClient = new UpstreamClient({
name: command,
transport,
onTransportError: (error) => {
process5.stderr.write(`Upstream MCP transport error: ${error.message}
`);
shutdown(1);
}
const mcps = /* @__PURE__ */ new Map([[SINGLE_MCP_NAME, { transport }]]);
const orchestrator = buildOrchestrator({
mcps,
namespaced: false,
transportErrorPrefix: () => "Upstream MCP"
});
const shutdown = (exitCode) => {
if (isShuttingDown) return;
isShuttingDown = true;
upstreamClient.disconnect().catch((error) => {
process5.stderr.write(
`dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
`
);
}).finally(() => process5.exit(exitCode));
};
try {
await upstreamClient.connect();
} catch (error) {
process5.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
`);
process5.exit(1);
}
let tools;
try {
tools = await upstreamClient.listTools();
} catch (error) {
process5.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
`);
process5.exit(1);
}
const catalog = ToolCatalog.fromFlat(tools);
const proxyServer = new ProxyServer({
catalog,
callTool: (name, input) => upstreamClient.callTool(name, input)
});
process5.on("SIGINT", () => shutdown(0));
process5.on("SIGTERM", () => shutdown(0));
process5.stdin.on("end", () => shutdown(0));
process5.stdin.on("close", () => shutdown(0));
try {
await proxyServer.start();
} catch (error) {
shutdown(1);
throw error;
}
await runProxy(orchestrator);
}
async function startProxyFromConfig(options = {}) {
let isShuttingDown = false;
const config = loadConfig(options);

@@ -734,10 +1611,25 @@ const mcps = /* @__PURE__ */ new Map();

}
const orchestrator = new Orchestrator({
const orchestrator = buildOrchestrator({
mcps,
namespaced: true,
transportErrorPrefix: (mcpName2) => `Upstream MCP "${mcpName2}"`
});
await runProxy(orchestrator);
}
var activeShutdown = { shutdown: null };
function buildOrchestrator(params) {
return new Orchestrator({
mcps: params.mcps,
namespaced: params.namespaced,
onTransportError: (mcpName2, error) => {
process5.stderr.write(`Upstream MCP "${mcpName2}" transport error: ${error.message}
`);
shutdown(1);
process6.stderr.write(
`${params.transportErrorPrefix(mcpName2)} transport error: ${error.message}
`
);
activeShutdown.shutdown?.(1);
}
});
}
async function runProxy(orchestrator) {
let isShuttingDown = false;
const shutdown = (exitCode) => {

@@ -747,23 +1639,52 @@ if (isShuttingDown) return;

orchestrator.disconnectAll().catch((error) => {
process5.stderr.write(
process6.stderr.write(
`dynmcp: error during disconnect: ${error instanceof Error ? error.message : String(error)}
`
);
}).finally(() => process5.exit(exitCode));
}).finally(() => process6.exit(exitCode));
};
activeShutdown.shutdown = shutdown;
try {
await orchestrator.connect();
} catch (error) {
process5.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
process6.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
`);
process5.exit(1);
process6.exit(1);
return;
}
const proxyServer = new ProxyServer({
catalog: orchestrator.catalog,
callTool: (name, input) => orchestrator.callTool(name, input)
catalog: () => orchestrator.catalog,
capabilities: orchestrator.capabilities,
callTool: (name, input, options) => orchestrator.callTool(name, input, options),
resources: orchestrator.capabilities.resources !== void 0 ? {
listResources: () => orchestrator.listResources(),
listResourceTemplates: () => orchestrator.listResourceTemplates(),
readResource: (uri, options) => orchestrator.readResource(uri, options),
subscribeResource: (uri, options) => orchestrator.subscribeResource(uri, options),
unsubscribeResource: (uri, options) => orchestrator.unsubscribeResource(uri, options)
} : void 0,
prompts: orchestrator.capabilities.prompts !== void 0 ? {
listPrompts: () => orchestrator.listPrompts(),
getPrompt: (name, args, options) => orchestrator.getPrompt(name, args, options)
} : void 0,
complete: orchestrator.capabilities.completions !== void 0 ? (params, options) => orchestrator.complete(params, options) : void 0,
setLoggingLevel: orchestrator.capabilities.logging !== void 0 ? (level, options) => orchestrator.setLoggingLevel(level, options) : void 0,
onRootsListChanged: () => orchestrator.broadcastRootsListChanged()
});
process5.on("SIGINT", () => shutdown(0));
process5.on("SIGTERM", () => shutdown(0));
process5.stdin.on("end", () => shutdown(0));
process5.stdin.on("close", () => shutdown(0));
orchestrator.setNotificationHandlers({
onToolsListChanged: () => proxyServer.sendToolListChanged(),
onResourcesListChanged: () => proxyServer.sendResourceListChanged(),
onResourceUpdated: (params) => proxyServer.sendResourceUpdated(params),
onPromptsListChanged: () => proxyServer.sendPromptListChanged(),
onLogMessage: (params) => proxyServer.sendLoggingMessage(params)
});
orchestrator.setServerRequestForwarders({
onCreateMessage: (params, options) => proxyServer.forwardCreateMessage(params, options),
onElicitInput: (params, options) => proxyServer.forwardElicitInput(params, options),
onListRoots: (params, options) => proxyServer.forwardListRoots(params, options)
});
process6.on("SIGINT", () => shutdown(0));
process6.on("SIGTERM", () => shutdown(0));
process6.stdin.on("end", () => shutdown(0));
process6.stdin.on("close", () => shutdown(0));
try {

@@ -788,16 +1709,13 @@ await proxyServer.start();

"\nExamples:\n dynmcp -- npx -y chrome-devtools-mcp@latest\n dynmcp --config ./mcp.json\n"
).option("-c, --config <path>", "Path to config file (JSON or YAML)").option(
"-e, --env <path>",
"Path to a .env file for environment variable interpolation"
).allowExcessArguments(true).passThroughOptions(true).action(async (_options, cmd) => {
const separatorIndex = process6.argv.indexOf("--");
).option("-c, --config <path>", "Path to config file (JSON or YAML)").option("-e, --env <path>", "Path to a .env file for environment variable interpolation").allowExcessArguments(true).passThroughOptions(true).action(async (_options, cmd) => {
const separatorIndex = process7.argv.indexOf("--");
const configPath = cmd.opts().config;
const envFilePath = cmd.opts().env;
if (separatorIndex !== -1) {
const [command, ...args] = process6.argv.slice(separatorIndex + 1);
const [command, ...args] = process7.argv.slice(separatorIndex + 1);
if (command === void 0) {
process6.stderr.write(
process7.stderr.write(
"dynmcp: no upstream command provided after --.\nUsage: dynmcp -- <command> [args...]\n"
);
process6.exit(1);
process7.exit(1);
}

@@ -807,5 +1725,5 @@ try {

} catch (error) {
process6.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
process7.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
`);
process6.exit(1);
process7.exit(1);
}

@@ -817,5 +1735,5 @@ return;

} catch (error) {
process6.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
process7.stderr.write(`dynmcp: ${error instanceof Error ? error.message : String(error)}
`);
process6.exit(1);
process7.exit(1);
}

@@ -825,7 +1743,7 @@ });

// src/index.ts
import process7 from "process";
import process8 from "process";
async function main() {
cli.parse(process7.argv);
cli.parse(process8.argv);
}
main();
//# sourceMappingURL=index.js.map
+2
-1
{
"name": "dynmcp",
"version": "0.2.0",
"version": "0.3.0",
"description": "Dynamic MCP context management tool for AI MCP-enabled agents and clients.",

@@ -83,2 +83,3 @@ "author": "Brandon Burrus <brandon@burrus.io>",

"@types/node": "^25.9.0",
"@vitest/coverage-v8": "^4.1.6",
"husky": "^9.1.7",

@@ -85,0 +86,0 @@ "tsup": "^8.5.1",

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display