dynmcp 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -135
- package/dist/index.cjs +549 -145
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +549 -145
- package/dist/index.js.map +1 -1
- package/package.json +3 -5
- package/schema/mcp-config.json +0 -126
package/dist/index.cjs
CHANGED
|
@@ -29,12 +29,12 @@ var import_commander = require("commander");
|
|
|
29
29
|
// package.json
|
|
30
30
|
var package_default = {
|
|
31
31
|
name: "dynmcp",
|
|
32
|
-
version: "0.
|
|
32
|
+
version: "0.4.1",
|
|
33
33
|
description: "Dynamic MCP context management tool for AI MCP-enabled agents and clients.",
|
|
34
34
|
author: "Brandon Burrus <brandon@burrus.io>",
|
|
35
35
|
license: "MIT",
|
|
36
36
|
type: "module",
|
|
37
|
-
homepage: "https://
|
|
37
|
+
homepage: "https://dynamicmcp.tools",
|
|
38
38
|
keywords: [
|
|
39
39
|
"mcp",
|
|
40
40
|
"model-context-protocol",
|
|
@@ -74,12 +74,10 @@ var package_default = {
|
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
76
|
files: [
|
|
77
|
-
"dist"
|
|
78
|
-
"schema"
|
|
77
|
+
"dist"
|
|
79
78
|
],
|
|
80
79
|
scripts: {
|
|
81
80
|
"generate:schema": "tsx scripts/generate-schema.ts",
|
|
82
|
-
prebuild: "tsx scripts/generate-schema.ts",
|
|
83
81
|
build: "tsup",
|
|
84
82
|
dev: "tsx src/index.ts",
|
|
85
83
|
typecheck: "tsc --noEmit",
|
|
@@ -133,8 +131,12 @@ var mcpName = import_zod.z.string().regex(MCP_NAME_PATTERN);
|
|
|
133
131
|
var envModeSchema = import_zod.z.enum(["enable", "dotenv", "process", "disable"]).describe(
|
|
134
132
|
'Controls environment variable interpolation in config values. "enable" (default) merges .env and process.env (.env wins). "dotenv" loads .env only. "process" uses process.env only. "disable" turns interpolation off.'
|
|
135
133
|
);
|
|
134
|
+
var description = import_zod.z.string().min(1, { message: "description must be a non-empty string" }).refine((value) => value.trim().length > 0, {
|
|
135
|
+
message: "description must not be whitespace-only"
|
|
136
|
+
}).optional();
|
|
136
137
|
var stdioTransport = import_zod.z.object({
|
|
137
138
|
transport: import_zod.z.literal("stdio"),
|
|
139
|
+
description,
|
|
138
140
|
command: import_zod.z.string(),
|
|
139
141
|
args: import_zod.z.array(import_zod.z.string()).optional(),
|
|
140
142
|
env: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
|
|
@@ -144,11 +146,13 @@ var httpUrl = import_zod.z.string().url().refine((u) => u.startsWith("http://")
|
|
|
144
146
|
});
|
|
145
147
|
var streamableHttpTransport = import_zod.z.object({
|
|
146
148
|
transport: import_zod.z.literal("streamable-http"),
|
|
149
|
+
description,
|
|
147
150
|
url: httpUrl,
|
|
148
151
|
headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
|
|
149
152
|
}).strict();
|
|
150
153
|
var sseTransport = import_zod.z.object({
|
|
151
154
|
transport: import_zod.z.literal("sse"),
|
|
155
|
+
description,
|
|
152
156
|
url: httpUrl,
|
|
153
157
|
headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional()
|
|
154
158
|
}).strict();
|
|
@@ -421,124 +425,87 @@ function aggregateCapabilities(upstreams) {
|
|
|
421
425
|
return aggregated;
|
|
422
426
|
}
|
|
423
427
|
|
|
424
|
-
// src/proxy/
|
|
425
|
-
var
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
constructor(tools, description) {
|
|
432
|
-
this.tools = tools;
|
|
433
|
-
this.discoverToolDescription = description;
|
|
434
|
-
}
|
|
435
|
-
static fromFlat(upstreamTools) {
|
|
436
|
-
const toolMap = /* @__PURE__ */ new Map();
|
|
437
|
-
for (const tool of upstreamTools) {
|
|
438
|
-
toolMap.set(tool.name, tool);
|
|
428
|
+
// src/proxy/lazy-registry.ts
|
|
429
|
+
var LazyRegistry = class {
|
|
430
|
+
entries = /* @__PURE__ */ new Map();
|
|
431
|
+
failureCounts = /* @__PURE__ */ new Map();
|
|
432
|
+
register(name, entry) {
|
|
433
|
+
if (this.entries.has(name)) {
|
|
434
|
+
throw new Error(`LazyRegistry: duplicate registration for "${name}"`);
|
|
439
435
|
}
|
|
440
|
-
|
|
441
|
-
return new _ToolCatalog(toolMap, description);
|
|
436
|
+
this.entries.set(name, entry);
|
|
442
437
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
for (const [mcpName2, tools] of groups) {
|
|
446
|
-
for (const tool of tools) {
|
|
447
|
-
toolMap.set(`${mcpName2}/${tool.name}`, tool);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
const description = buildGroupedDescription(groups);
|
|
451
|
-
return new _ToolCatalog(toolMap, description);
|
|
438
|
+
has(name) {
|
|
439
|
+
return this.entries.has(name);
|
|
452
440
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if (tool === void 0) {
|
|
456
|
-
const sortedNames = [...this.tools.keys()].sort().join(", ");
|
|
457
|
-
return `Unknown tool: "${toolName}". Available tools: ${sortedNames}`;
|
|
458
|
-
}
|
|
459
|
-
return buildToolDetailsString(toolName, tool);
|
|
441
|
+
get(name) {
|
|
442
|
+
return this.entries.get(name);
|
|
460
443
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
function buildGroupedDescription(groups) {
|
|
472
|
-
const sortedMcpNames = [...groups.keys()].sort();
|
|
473
|
-
const sections = sortedMcpNames.map((mcpName2) => {
|
|
474
|
-
const tools = groups.get(mcpName2);
|
|
475
|
-
const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
476
|
-
const toolLines = sortedTools.map((tool) => `- ${mcpName2}/${tool.name}: ${tool.description}`).join("\n");
|
|
477
|
-
return `${mcpName2}:
|
|
478
|
-
${toolLines}`;
|
|
479
|
-
});
|
|
480
|
-
return `${DISCOVER_TOOL_PREAMBLE}
|
|
481
|
-
|
|
482
|
-
<tools>
|
|
483
|
-
${sections.join("\n\n")}
|
|
484
|
-
</tools>`;
|
|
485
|
-
}
|
|
486
|
-
function buildToolDetailsString(displayName, tool) {
|
|
487
|
-
const lines = [
|
|
488
|
-
`Tool: ${displayName}`,
|
|
489
|
-
`Description: ${tool.description}`,
|
|
490
|
-
"",
|
|
491
|
-
"Input Schema:",
|
|
492
|
-
JSON.stringify(tool.inputSchema, null, 2)
|
|
493
|
-
];
|
|
494
|
-
if (tool.outputSchema !== void 0) {
|
|
495
|
-
lines.push("", "Output Schema:", JSON.stringify(tool.outputSchema, null, 2));
|
|
496
|
-
}
|
|
497
|
-
const annotationLines = buildAnnotationLines(tool);
|
|
498
|
-
if (annotationLines.length > 0) {
|
|
499
|
-
lines.push("", "Annotations:", ...annotationLines);
|
|
444
|
+
/**
|
|
445
|
+
* Records a failed load attempt and returns the new total. The orchestrator's
|
|
446
|
+
* retry-budget logic uses this to decide whether to evict the entry. A
|
|
447
|
+
* subsequent successful load (or {@link take}) clears the count.
|
|
448
|
+
*/
|
|
449
|
+
recordFailure(name) {
|
|
450
|
+
const next = (this.failureCounts.get(name) ?? 0) + 1;
|
|
451
|
+
this.failureCounts.set(name, next);
|
|
452
|
+
return next;
|
|
500
453
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
function buildAnnotationLines(tool) {
|
|
504
|
-
if (tool.annotations === void 0) {
|
|
505
|
-
return [];
|
|
454
|
+
failureCount(name) {
|
|
455
|
+
return this.failureCounts.get(name) ?? 0;
|
|
506
456
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
457
|
+
/**
|
|
458
|
+
* Returns the descriptions of every still-lazy MCP in insertion order. Consumed
|
|
459
|
+
* by {@link ToolCatalog.fromGroupedWithLazy} to render the `<mcp_servers>` block.
|
|
460
|
+
*/
|
|
461
|
+
descriptions() {
|
|
462
|
+
const result = /* @__PURE__ */ new Map();
|
|
463
|
+
for (const [name, entry] of this.entries) {
|
|
464
|
+
result.set(name, entry.description);
|
|
465
|
+
}
|
|
466
|
+
return result;
|
|
514
467
|
}
|
|
515
|
-
|
|
516
|
-
|
|
468
|
+
/**
|
|
469
|
+
* Returns the names of every still-lazy MCP in insertion order. Used by error
|
|
470
|
+
* messages that need to hint the agent at what is loadable.
|
|
471
|
+
*/
|
|
472
|
+
names() {
|
|
473
|
+
return [...this.entries.keys()];
|
|
517
474
|
}
|
|
518
|
-
|
|
519
|
-
|
|
475
|
+
size() {
|
|
476
|
+
return this.entries.size;
|
|
520
477
|
}
|
|
521
|
-
|
|
522
|
-
|
|
478
|
+
/**
|
|
479
|
+
* Removes and returns the entry, signalling that the upstream has been (or is
|
|
480
|
+
* about to be) promoted to a connected client (or evicted after exhausting
|
|
481
|
+
* its retry budget). The caller is responsible for ensuring the promotion
|
|
482
|
+
* actually succeeds — failed loads should re-register via {@link register} to
|
|
483
|
+
* roll back the state. Clears the failure count along with the entry.
|
|
484
|
+
*/
|
|
485
|
+
take(name) {
|
|
486
|
+
const entry = this.entries.get(name);
|
|
487
|
+
if (entry === void 0) return void 0;
|
|
488
|
+
this.entries.delete(name);
|
|
489
|
+
this.failureCounts.delete(name);
|
|
490
|
+
return entry;
|
|
523
491
|
}
|
|
524
|
-
|
|
525
|
-
}
|
|
492
|
+
};
|
|
526
493
|
|
|
527
494
|
// src/proxy/notification-forwarder.ts
|
|
528
495
|
var NotificationForwarder = class {
|
|
529
|
-
constructor(registry, resourceRouter, promptRouter, toolsByMcp,
|
|
496
|
+
constructor(registry, resourceRouter, promptRouter, toolsByMcp, rebuildToolCatalog, namespaced) {
|
|
530
497
|
this.registry = registry;
|
|
531
498
|
this.resourceRouter = resourceRouter;
|
|
532
499
|
this.promptRouter = promptRouter;
|
|
533
500
|
this.toolsByMcp = toolsByMcp;
|
|
534
|
-
this.
|
|
501
|
+
this.rebuildToolCatalog = rebuildToolCatalog;
|
|
535
502
|
this.namespaced = namespaced;
|
|
536
503
|
}
|
|
537
504
|
registry;
|
|
538
505
|
resourceRouter;
|
|
539
506
|
promptRouter;
|
|
540
507
|
toolsByMcp;
|
|
541
|
-
|
|
508
|
+
rebuildToolCatalog;
|
|
542
509
|
namespaced;
|
|
543
510
|
hostHandlers = {};
|
|
544
511
|
setHostHandlers(handlers) {
|
|
@@ -549,8 +516,7 @@ var NotificationForwarder = class {
|
|
|
549
516
|
if (client === void 0) return;
|
|
550
517
|
const tools = await client.listTools().catch(() => []);
|
|
551
518
|
this.toolsByMcp.set(mcpName2, tools);
|
|
552
|
-
|
|
553
|
-
this.setToolCatalog(rebuilt);
|
|
519
|
+
this.rebuildToolCatalog();
|
|
554
520
|
await this.hostHandlers.onToolsListChanged?.();
|
|
555
521
|
}
|
|
556
522
|
async handleResourcesListChanged(mcpName2) {
|
|
@@ -568,6 +534,22 @@ var NotificationForwarder = class {
|
|
|
568
534
|
async handleResourceUpdated(params) {
|
|
569
535
|
await this.hostHandlers.onResourceUpdated?.(params);
|
|
570
536
|
}
|
|
537
|
+
/**
|
|
538
|
+
* Fire-only emitters. Unlike `handleXListChanged`, these do not re-fetch the
|
|
539
|
+
* affected data from any upstream — the caller has already populated the relevant
|
|
540
|
+
* state directly. Used by the orchestrator's load_mcp pipeline, which already has
|
|
541
|
+
* the freshly-queried tools/resources/prompts in hand and just needs to nudge the
|
|
542
|
+
* host to refetch.
|
|
543
|
+
*/
|
|
544
|
+
async notifyToolsListChanged() {
|
|
545
|
+
await this.hostHandlers.onToolsListChanged?.();
|
|
546
|
+
}
|
|
547
|
+
async notifyResourcesListChanged() {
|
|
548
|
+
await this.hostHandlers.onResourcesListChanged?.();
|
|
549
|
+
}
|
|
550
|
+
async notifyPromptsListChanged() {
|
|
551
|
+
await this.hostHandlers.onPromptsListChanged?.();
|
|
552
|
+
}
|
|
571
553
|
async handlePromptsListChanged(mcpName2) {
|
|
572
554
|
const router = this.promptRouter();
|
|
573
555
|
const client = this.registry.get(mcpName2);
|
|
@@ -630,6 +612,14 @@ var PromptRouter = class {
|
|
|
630
612
|
collisions() {
|
|
631
613
|
return this.detectedCollisions;
|
|
632
614
|
}
|
|
615
|
+
/**
|
|
616
|
+
* Returns the prompts contributed by a single upstream MCP. Used by the load_mcp
|
|
617
|
+
* pipeline to construct its structured response. Returns an empty array if the MCP
|
|
618
|
+
* has not contributed any prompts (or if `mcpName` is unknown).
|
|
619
|
+
*/
|
|
620
|
+
promptsFor(mcpName2) {
|
|
621
|
+
return this.perMcp.get(mcpName2) ?? [];
|
|
622
|
+
}
|
|
633
623
|
rebuild() {
|
|
634
624
|
this.nameOwners = /* @__PURE__ */ new Map();
|
|
635
625
|
const collisions = [];
|
|
@@ -718,6 +708,19 @@ var ResourceRouter = class {
|
|
|
718
708
|
collisions() {
|
|
719
709
|
return this.detectedCollisions;
|
|
720
710
|
}
|
|
711
|
+
/**
|
|
712
|
+
* Returns the resources contributed by a single upstream MCP. Used by the load_mcp
|
|
713
|
+
* pipeline to construct its structured response, which lists what a just-loaded MCP
|
|
714
|
+
* (or an already-loaded MCP, in the idempotent no-op path) brought to the proxy.
|
|
715
|
+
* Returns an empty array if the MCP has not contributed any resources (or if
|
|
716
|
+
* `mcpName` is unknown).
|
|
717
|
+
*/
|
|
718
|
+
resourcesFor(mcpName2) {
|
|
719
|
+
return this.perMcp.get(mcpName2)?.resources ?? [];
|
|
720
|
+
}
|
|
721
|
+
templatesFor(mcpName2) {
|
|
722
|
+
return this.perMcp.get(mcpName2)?.templates ?? [];
|
|
723
|
+
}
|
|
721
724
|
rebuild() {
|
|
722
725
|
this.uriOwners = /* @__PURE__ */ new Map();
|
|
723
726
|
this.templateOwners = [];
|
|
@@ -749,6 +752,145 @@ function literalPrefixOf(uriTemplate) {
|
|
|
749
752
|
return idx === -1 ? uriTemplate : uriTemplate.slice(0, idx);
|
|
750
753
|
}
|
|
751
754
|
|
|
755
|
+
// src/proxy/tool-catalog.ts
|
|
756
|
+
var DISCOVER_TOOL_PREAMBLE = `Use this tool to look up the full schema of a tool before calling it with use_tool.
|
|
757
|
+
Call discover_tool with a tool name from the list below to get its complete description,
|
|
758
|
+
input parameters, and output schema. Always discover a tool before using it.`;
|
|
759
|
+
var DYNAMIC_DISCOVERY_PREAMBLE = `Some MCP servers below are not loaded yet and are listed under <mcp_servers> with a
|
|
760
|
+
short description of what they do. To make a server's tools (and any resources or
|
|
761
|
+
prompts it exposes) available, call load_mcp with its name. Once loaded, the server's
|
|
762
|
+
tools will appear in the <tools> list and become callable via use_tool. Loading is
|
|
763
|
+
permanent for the remainder of this session.`;
|
|
764
|
+
var NO_TOOLS_LOADED_FOOTER = "No tools are currently loaded. Call load_mcp to make a server's tools available.";
|
|
765
|
+
var ToolCatalog = class _ToolCatalog {
|
|
766
|
+
tools;
|
|
767
|
+
discoverToolDescription;
|
|
768
|
+
constructor(tools, description2) {
|
|
769
|
+
this.tools = tools;
|
|
770
|
+
this.discoverToolDescription = description2;
|
|
771
|
+
}
|
|
772
|
+
static fromFlat(upstreamTools) {
|
|
773
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
774
|
+
for (const tool of upstreamTools) {
|
|
775
|
+
toolMap.set(tool.name, tool);
|
|
776
|
+
}
|
|
777
|
+
const description2 = buildFlatDescription(upstreamTools);
|
|
778
|
+
return new _ToolCatalog(toolMap, description2);
|
|
779
|
+
}
|
|
780
|
+
static fromGrouped(groups) {
|
|
781
|
+
return _ToolCatalog.fromGroupedWithLazy(groups, /* @__PURE__ */ new Map());
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Same as {@link fromGrouped} but additionally accepts a map of lazy upstream MCPs
|
|
785
|
+
* (those declared with a `description` field but not yet loaded). When the map is
|
|
786
|
+
* non-empty, the rendered `discover_tool` description includes a `<mcp_servers>`
|
|
787
|
+
* block listing them with their descriptions and an explanatory paragraph telling
|
|
788
|
+
* the agent how to call `load_mcp`. When `groups` is empty, the `<tools>` block is
|
|
789
|
+
* omitted in favor of a trailing sentence directing the agent to `load_mcp`.
|
|
790
|
+
*/
|
|
791
|
+
static fromGroupedWithLazy(groups, lazyDescriptions) {
|
|
792
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
793
|
+
for (const [mcpName2, tools] of groups) {
|
|
794
|
+
for (const tool of tools) {
|
|
795
|
+
toolMap.set(`${mcpName2}/${tool.name}`, tool);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const description2 = buildGroupedDescription(groups, lazyDescriptions);
|
|
799
|
+
return new _ToolCatalog(toolMap, description2);
|
|
800
|
+
}
|
|
801
|
+
getToolDetails(toolName) {
|
|
802
|
+
const tool = this.tools.get(toolName);
|
|
803
|
+
if (tool === void 0) {
|
|
804
|
+
const sortedNames = [...this.tools.keys()].sort().join(", ");
|
|
805
|
+
return `Unknown tool: "${toolName}". Available tools: ${sortedNames}`;
|
|
806
|
+
}
|
|
807
|
+
return buildToolDetailsString(toolName, tool);
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
function buildFlatDescription(tools) {
|
|
811
|
+
const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
812
|
+
const toolLines = sortedTools.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
|
|
813
|
+
return `${DISCOVER_TOOL_PREAMBLE}
|
|
814
|
+
|
|
815
|
+
<tools>
|
|
816
|
+
${toolLines}
|
|
817
|
+
</tools>`;
|
|
818
|
+
}
|
|
819
|
+
function buildGroupedDescription(groups, lazyDescriptions) {
|
|
820
|
+
const parts = [DISCOVER_TOOL_PREAMBLE];
|
|
821
|
+
if (lazyDescriptions.size > 0) {
|
|
822
|
+
parts.push(DYNAMIC_DISCOVERY_PREAMBLE);
|
|
823
|
+
parts.push(buildMcpServersBlock(lazyDescriptions));
|
|
824
|
+
}
|
|
825
|
+
if (groups.size > 0) {
|
|
826
|
+
parts.push(buildToolsBlock(groups));
|
|
827
|
+
} else if (lazyDescriptions.size > 0) {
|
|
828
|
+
parts.push(NO_TOOLS_LOADED_FOOTER);
|
|
829
|
+
} else {
|
|
830
|
+
parts.push("<tools>\n</tools>");
|
|
831
|
+
}
|
|
832
|
+
return parts.join("\n\n");
|
|
833
|
+
}
|
|
834
|
+
function buildToolsBlock(groups) {
|
|
835
|
+
const sortedMcpNames = [...groups.keys()].sort();
|
|
836
|
+
const sections = sortedMcpNames.map((mcpName2) => {
|
|
837
|
+
const tools = groups.get(mcpName2);
|
|
838
|
+
const sortedTools = [...tools].sort((a, b) => a.name.localeCompare(b.name));
|
|
839
|
+
const toolLines = sortedTools.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
|
|
840
|
+
return `${mcpName2}:
|
|
841
|
+
${toolLines}`;
|
|
842
|
+
});
|
|
843
|
+
return `<tools>
|
|
844
|
+
${sections.join("\n\n")}
|
|
845
|
+
</tools>`;
|
|
846
|
+
}
|
|
847
|
+
function buildMcpServersBlock(lazyDescriptions) {
|
|
848
|
+
const lines = [...lazyDescriptions].map(([name, desc]) => `- ${name}: ${desc}`).join("\n");
|
|
849
|
+
return `<mcp_servers>
|
|
850
|
+
${lines}
|
|
851
|
+
</mcp_servers>`;
|
|
852
|
+
}
|
|
853
|
+
function buildToolDetailsString(displayName, tool) {
|
|
854
|
+
const lines = [
|
|
855
|
+
`Tool: ${displayName}`,
|
|
856
|
+
`Description: ${tool.description}`,
|
|
857
|
+
"",
|
|
858
|
+
"Input Schema:",
|
|
859
|
+
JSON.stringify(tool.inputSchema, null, 2)
|
|
860
|
+
];
|
|
861
|
+
if (tool.outputSchema !== void 0) {
|
|
862
|
+
lines.push("", "Output Schema:", JSON.stringify(tool.outputSchema, null, 2));
|
|
863
|
+
}
|
|
864
|
+
const annotationLines = buildAnnotationLines(tool);
|
|
865
|
+
if (annotationLines.length > 0) {
|
|
866
|
+
lines.push("", "Annotations:", ...annotationLines);
|
|
867
|
+
}
|
|
868
|
+
return lines.join("\n");
|
|
869
|
+
}
|
|
870
|
+
function buildAnnotationLines(tool) {
|
|
871
|
+
if (tool.annotations === void 0) {
|
|
872
|
+
return [];
|
|
873
|
+
}
|
|
874
|
+
const { annotations } = tool;
|
|
875
|
+
const lines = [];
|
|
876
|
+
if (annotations.title !== void 0) {
|
|
877
|
+
lines.push(`- title: ${annotations.title}`);
|
|
878
|
+
}
|
|
879
|
+
if (annotations.readOnlyHint !== void 0) {
|
|
880
|
+
lines.push(`- readOnlyHint: ${annotations.readOnlyHint}`);
|
|
881
|
+
}
|
|
882
|
+
if (annotations.destructiveHint !== void 0) {
|
|
883
|
+
lines.push(`- destructiveHint: ${annotations.destructiveHint}`);
|
|
884
|
+
}
|
|
885
|
+
if (annotations.idempotentHint !== void 0) {
|
|
886
|
+
lines.push(`- idempotentHint: ${annotations.idempotentHint}`);
|
|
887
|
+
}
|
|
888
|
+
if (annotations.openWorldHint !== void 0) {
|
|
889
|
+
lines.push(`- openWorldHint: ${annotations.openWorldHint}`);
|
|
890
|
+
}
|
|
891
|
+
return lines;
|
|
892
|
+
}
|
|
893
|
+
|
|
752
894
|
// src/proxy/upstream-client.ts
|
|
753
895
|
var import_node_process3 = __toESM(require("process"), 1);
|
|
754
896
|
var import_client = require("@modelcontextprotocol/sdk/client/index.js");
|
|
@@ -974,6 +1116,41 @@ var UpstreamRegistry = class {
|
|
|
974
1116
|
throw error;
|
|
975
1117
|
}
|
|
976
1118
|
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Connects a single additional upstream. Unlike {@link connectAll}, a failure here
|
|
1121
|
+
* leaves any other connected clients untouched — the caller (typically the
|
|
1122
|
+
* Orchestrator's `loadMcp` pipeline) needs that isolation because other lazy MCPs
|
|
1123
|
+
* may have already been promoted to loaded, or eager MCPs are still healthy.
|
|
1124
|
+
*
|
|
1125
|
+
* Throws if `mcpName` is already in the registry; callers should check beforehand
|
|
1126
|
+
* (the orchestrator handles the idempotency-success case before reaching here).
|
|
1127
|
+
*/
|
|
1128
|
+
async connectOne(mcpName2, config) {
|
|
1129
|
+
if (this.clients.has(mcpName2)) {
|
|
1130
|
+
throw new Error(`UpstreamRegistry: "${mcpName2}" is already connected`);
|
|
1131
|
+
}
|
|
1132
|
+
const client = new UpstreamClient({
|
|
1133
|
+
name: mcpName2,
|
|
1134
|
+
transport: config.transport,
|
|
1135
|
+
onTransportError: config.onTransportError,
|
|
1136
|
+
notifications: config.notifications,
|
|
1137
|
+
serverRequests: config.serverRequests
|
|
1138
|
+
});
|
|
1139
|
+
await client.connect();
|
|
1140
|
+
this.clients.set(mcpName2, client);
|
|
1141
|
+
return client;
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Disconnects and removes a single upstream. Used by `loadMcp` to roll back a
|
|
1145
|
+
* partially-loaded MCP when a post-connect catalog query fails. No-op if the name
|
|
1146
|
+
* is not present.
|
|
1147
|
+
*/
|
|
1148
|
+
async deleteOne(mcpName2) {
|
|
1149
|
+
const client = this.clients.get(mcpName2);
|
|
1150
|
+
if (client === void 0) return;
|
|
1151
|
+
this.clients.delete(mcpName2);
|
|
1152
|
+
await client.disconnect();
|
|
1153
|
+
}
|
|
977
1154
|
get(mcpName2) {
|
|
978
1155
|
return this.clients.get(mcpName2);
|
|
979
1156
|
}
|
|
@@ -1003,10 +1180,18 @@ var UpstreamRegistry = class {
|
|
|
1003
1180
|
};
|
|
1004
1181
|
|
|
1005
1182
|
// src/proxy/orchestrator.ts
|
|
1183
|
+
var MAX_LOAD_ATTEMPTS = 3;
|
|
1006
1184
|
var Orchestrator = class {
|
|
1007
1185
|
config;
|
|
1008
1186
|
registry = new UpstreamRegistry();
|
|
1187
|
+
lazyRegistry = new LazyRegistry();
|
|
1009
1188
|
toolsByMcp = /* @__PURE__ */ new Map();
|
|
1189
|
+
/**
|
|
1190
|
+
* In-flight loads, keyed by mcpName. Used by {@link loadMcp} to coalesce concurrent
|
|
1191
|
+
* calls for the same name onto a single underlying connection attempt — per SPEC.md
|
|
1192
|
+
* § "Dynamic Discovery > Concurrency".
|
|
1193
|
+
*/
|
|
1194
|
+
inFlightLoads = /* @__PURE__ */ new Map();
|
|
1010
1195
|
resourceRouter = null;
|
|
1011
1196
|
promptRouter = null;
|
|
1012
1197
|
toolCatalog = null;
|
|
@@ -1014,9 +1199,14 @@ var Orchestrator = class {
|
|
|
1014
1199
|
serverRequestForwarders = {};
|
|
1015
1200
|
forwarder;
|
|
1016
1201
|
constructor(config) {
|
|
1017
|
-
if (!config.namespaced && config.
|
|
1202
|
+
if (!config.namespaced && config.eagerMcps.size !== 1) {
|
|
1203
|
+
throw new Error(
|
|
1204
|
+
`Single-MCP (non-namespaced) mode requires exactly one upstream; got ${config.eagerMcps.size}.`
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
if (!config.namespaced && config.lazyMcps !== void 0 && config.lazyMcps.size > 0) {
|
|
1018
1208
|
throw new Error(
|
|
1019
|
-
|
|
1209
|
+
"Single-MCP (non-namespaced) mode does not support lazy upstreams (descriptions)."
|
|
1020
1210
|
);
|
|
1021
1211
|
}
|
|
1022
1212
|
this.config = config;
|
|
@@ -1025,9 +1215,7 @@ var Orchestrator = class {
|
|
|
1025
1215
|
() => this.resourceRouter,
|
|
1026
1216
|
() => this.promptRouter,
|
|
1027
1217
|
this.toolsByMcp,
|
|
1028
|
-
(
|
|
1029
|
-
this.toolCatalog = catalog;
|
|
1030
|
-
},
|
|
1218
|
+
() => this.rebuildToolCatalog(),
|
|
1031
1219
|
this.config.namespaced
|
|
1032
1220
|
);
|
|
1033
1221
|
}
|
|
@@ -1037,32 +1225,26 @@ var Orchestrator = class {
|
|
|
1037
1225
|
setServerRequestForwarders(forwarders) {
|
|
1038
1226
|
this.serverRequestForwarders = forwarders;
|
|
1039
1227
|
}
|
|
1228
|
+
/**
|
|
1229
|
+
* True when the orchestrator was configured with at least one lazy upstream MCP.
|
|
1230
|
+
* The `index.ts` wiring uses this to decide whether to register the `load_mcp`
|
|
1231
|
+
* meta-tool with the host-facing {@link ProxyServer}.
|
|
1232
|
+
*/
|
|
1233
|
+
get hasDynamicDiscovery() {
|
|
1234
|
+
return this.config.lazyMcps !== void 0 && this.config.lazyMcps.size > 0;
|
|
1235
|
+
}
|
|
1040
1236
|
async connect() {
|
|
1041
|
-
const
|
|
1042
|
-
const
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
{
|
|
1048
|
-
transport,
|
|
1049
|
-
onTransportError: (error) => {
|
|
1050
|
-
this.config.onTransportError?.(mcpName2, error);
|
|
1051
|
-
},
|
|
1052
|
-
notifications: {
|
|
1053
|
-
onToolsListChanged: () => this.forwarder.handleToolsListChanged(mcpName2),
|
|
1054
|
-
onResourcesListChanged: () => this.forwarder.handleResourcesListChanged(mcpName2),
|
|
1055
|
-
onResourceUpdated: (params) => this.forwarder.handleResourceUpdated(params),
|
|
1056
|
-
onPromptsListChanged: () => this.forwarder.handlePromptsListChanged(mcpName2),
|
|
1057
|
-
onLogMessage: (params) => this.forwarder.handleLogMessage(mcpName2, params)
|
|
1058
|
-
},
|
|
1059
|
-
serverRequests: {
|
|
1060
|
-
onCreateMessage: (params, opts) => this.forwardCreateMessage(params, opts),
|
|
1061
|
-
onElicitInput: (params, opts) => this.forwardElicitInput(params, opts),
|
|
1062
|
-
onListRoots: (params, opts) => this.forwardListRoots(params, opts)
|
|
1063
|
-
}
|
|
1237
|
+
const allNames = [...this.config.eagerMcps.keys(), ...this.config.lazyMcps?.keys() ?? []];
|
|
1238
|
+
const resourceRouter = new ResourceRouter(allNames);
|
|
1239
|
+
const promptRouter = new PromptRouter(allNames);
|
|
1240
|
+
if (this.config.lazyMcps !== void 0) {
|
|
1241
|
+
for (const [name, { transport, description: description2 }] of this.config.lazyMcps) {
|
|
1242
|
+
this.lazyRegistry.register(name, { transport, description: description2 });
|
|
1064
1243
|
}
|
|
1065
|
-
|
|
1244
|
+
}
|
|
1245
|
+
const upstreamEntries = [
|
|
1246
|
+
...this.config.eagerMcps
|
|
1247
|
+
].map(([mcpName2, { transport }]) => [mcpName2, this.buildUpstreamConfig(mcpName2, transport)]);
|
|
1066
1248
|
await this.registry.connectAll(upstreamEntries);
|
|
1067
1249
|
const capabilityList = [];
|
|
1068
1250
|
this.toolsByMcp.clear();
|
|
@@ -1084,10 +1266,10 @@ var Orchestrator = class {
|
|
|
1084
1266
|
promptRouter.setPrompts(mcpName2, prompts);
|
|
1085
1267
|
}
|
|
1086
1268
|
}
|
|
1087
|
-
this.toolCatalog = this.config.namespaced ? ToolCatalog.fromGrouped(this.toolsByMcp) : ToolCatalog.fromFlat([...this.toolsByMcp.values()][0] ?? []);
|
|
1088
|
-
this.aggregatedCapabilities = aggregateCapabilities(capabilityList);
|
|
1089
1269
|
this.resourceRouter = resourceRouter;
|
|
1090
1270
|
this.promptRouter = promptRouter;
|
|
1271
|
+
this.aggregatedCapabilities = aggregateCapabilities(capabilityList);
|
|
1272
|
+
this.rebuildToolCatalog();
|
|
1091
1273
|
logCollisions(resourceRouter, promptRouter);
|
|
1092
1274
|
}
|
|
1093
1275
|
async disconnectAll() {
|
|
@@ -1097,6 +1279,177 @@ var Orchestrator = class {
|
|
|
1097
1279
|
this.aggregatedCapabilities = null;
|
|
1098
1280
|
this.resourceRouter = null;
|
|
1099
1281
|
this.promptRouter = null;
|
|
1282
|
+
this.inFlightLoads.clear();
|
|
1283
|
+
}
|
|
1284
|
+
// === Dynamic discovery ===
|
|
1285
|
+
/**
|
|
1286
|
+
* Loads a lazy upstream MCP on demand. Implements the semantics of SPEC.md §
|
|
1287
|
+
* "Tools > load_mcp" and § "Dynamic Discovery > Lifecycle of a Lazy MCP":
|
|
1288
|
+
*
|
|
1289
|
+
* - Already-loaded (eager or previously-loaded lazy) names succeed as a no-op
|
|
1290
|
+
* returning the current listing; no notifications fire.
|
|
1291
|
+
* - Unknown names throw an error that hints at the still-lazy alternatives.
|
|
1292
|
+
* - Concurrent calls for the same name coalesce onto the same in-flight load.
|
|
1293
|
+
* - A failure during connect/initialize/catalog-query rolls back atomically:
|
|
1294
|
+
* the upstream is disconnected, the lazy entry stays registered, no host
|
|
1295
|
+
* notifications fire.
|
|
1296
|
+
* - On success the loaded MCP is promoted into the connected registry, its
|
|
1297
|
+
* tools/resources/prompts populate the routers, the discover_tool catalog
|
|
1298
|
+
* is regenerated, and the host receives `tools/list_changed` plus
|
|
1299
|
+
* `resources/list_changed` and/or `prompts/list_changed` for any non-empty
|
|
1300
|
+
* surface the MCP contributed.
|
|
1301
|
+
*/
|
|
1302
|
+
async loadMcp(mcpName2) {
|
|
1303
|
+
if (this.registry.get(mcpName2) !== void 0) {
|
|
1304
|
+
return this.getListing(mcpName2);
|
|
1305
|
+
}
|
|
1306
|
+
const inFlight = this.inFlightLoads.get(mcpName2);
|
|
1307
|
+
if (inFlight !== void 0) {
|
|
1308
|
+
return inFlight;
|
|
1309
|
+
}
|
|
1310
|
+
if (!this.lazyRegistry.has(mcpName2)) {
|
|
1311
|
+
const lazyNames = this.lazyRegistry.names().join(", ");
|
|
1312
|
+
const hint = lazyNames.length > 0 ? lazyNames : "(none)";
|
|
1313
|
+
throw new Error(`Unknown MCP server: "${mcpName2}". Available servers to load: ${hint}`);
|
|
1314
|
+
}
|
|
1315
|
+
const loadPromise = this.runLoadPipeline(mcpName2).finally(() => {
|
|
1316
|
+
this.inFlightLoads.delete(mcpName2);
|
|
1317
|
+
});
|
|
1318
|
+
this.inFlightLoads.set(mcpName2, loadPromise);
|
|
1319
|
+
return loadPromise;
|
|
1320
|
+
}
|
|
1321
|
+
async runLoadPipeline(mcpName2) {
|
|
1322
|
+
const entry = this.lazyRegistry.get(mcpName2);
|
|
1323
|
+
if (entry === void 0) {
|
|
1324
|
+
throw new Error(`Internal error: lazy entry "${mcpName2}" vanished mid-load.`);
|
|
1325
|
+
}
|
|
1326
|
+
const client = await this.registry.connectOne(
|
|
1327
|
+
mcpName2,
|
|
1328
|
+
this.buildUpstreamConfig(mcpName2, entry.transport)
|
|
1329
|
+
);
|
|
1330
|
+
let tools;
|
|
1331
|
+
let resources = [];
|
|
1332
|
+
let templates = [];
|
|
1333
|
+
let prompts = [];
|
|
1334
|
+
let caps;
|
|
1335
|
+
try {
|
|
1336
|
+
caps = client.getCapabilities();
|
|
1337
|
+
tools = await client.listTools();
|
|
1338
|
+
if (caps?.resources !== void 0) {
|
|
1339
|
+
[resources, templates] = await Promise.all([
|
|
1340
|
+
client.listResources(),
|
|
1341
|
+
client.listResourceTemplates()
|
|
1342
|
+
]);
|
|
1343
|
+
}
|
|
1344
|
+
if (caps?.prompts !== void 0) {
|
|
1345
|
+
prompts = await client.listPrompts();
|
|
1346
|
+
}
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
await this.registry.deleteOne(mcpName2);
|
|
1349
|
+
const failures = this.lazyRegistry.recordFailure(mcpName2);
|
|
1350
|
+
if (failures >= MAX_LOAD_ATTEMPTS) {
|
|
1351
|
+
this.lazyRegistry.take(mcpName2);
|
|
1352
|
+
this.rebuildToolCatalog();
|
|
1353
|
+
await this.forwarder.notifyToolsListChanged();
|
|
1354
|
+
const base = error instanceof Error ? error.message : String(error);
|
|
1355
|
+
throw new Error(
|
|
1356
|
+
`Failed to load "${mcpName2}" after ${failures} attempts; the server will no longer be offered for discovery. Underlying error: ${base}`
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
throw error;
|
|
1360
|
+
}
|
|
1361
|
+
this.lazyRegistry.take(mcpName2);
|
|
1362
|
+
this.toolsByMcp.set(mcpName2, tools);
|
|
1363
|
+
const resourceRouter = this.requireResourceRouter();
|
|
1364
|
+
const promptRouter = this.requirePromptRouter();
|
|
1365
|
+
if (caps?.resources !== void 0) {
|
|
1366
|
+
resourceRouter.setResources(mcpName2, resources);
|
|
1367
|
+
resourceRouter.setTemplates(mcpName2, templates);
|
|
1368
|
+
}
|
|
1369
|
+
if (caps?.prompts !== void 0) {
|
|
1370
|
+
promptRouter.setPrompts(mcpName2, prompts);
|
|
1371
|
+
}
|
|
1372
|
+
this.rebuildToolCatalog();
|
|
1373
|
+
await this.forwarder.notifyToolsListChanged();
|
|
1374
|
+
if (caps?.resources !== void 0 && (resources.length > 0 || templates.length > 0)) {
|
|
1375
|
+
await this.forwarder.notifyResourcesListChanged();
|
|
1376
|
+
}
|
|
1377
|
+
if (caps?.prompts !== void 0 && prompts.length > 0) {
|
|
1378
|
+
await this.forwarder.notifyPromptsListChanged();
|
|
1379
|
+
}
|
|
1380
|
+
return this.getListing(mcpName2);
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Builds the structured response shape documented for `load_mcp` from existing
|
|
1384
|
+
* per-MCP state. Pulled out into a helper so the no-op path (already-loaded)
|
|
1385
|
+
* and the success path share one source of truth.
|
|
1386
|
+
*/
|
|
1387
|
+
getListing(mcpName2) {
|
|
1388
|
+
const tools = this.toolsByMcp.get(mcpName2) ?? [];
|
|
1389
|
+
const namespacedToolName = (name) => this.config.namespaced ? `${mcpName2}/${name}` : name;
|
|
1390
|
+
const resources = this.resourceRouter?.resourcesFor(mcpName2) ?? [];
|
|
1391
|
+
const templates = this.resourceRouter?.templatesFor(mcpName2) ?? [];
|
|
1392
|
+
const prompts = this.promptRouter?.promptsFor(mcpName2) ?? [];
|
|
1393
|
+
return {
|
|
1394
|
+
mcp_name: mcpName2,
|
|
1395
|
+
tools: tools.map((tool) => ({
|
|
1396
|
+
name: namespacedToolName(tool.name),
|
|
1397
|
+
description: tool.description
|
|
1398
|
+
})),
|
|
1399
|
+
resources: resources.map((resource) => ({
|
|
1400
|
+
uri: resource.uri,
|
|
1401
|
+
name: resource.name,
|
|
1402
|
+
description: resource.description,
|
|
1403
|
+
mimeType: resource.mimeType
|
|
1404
|
+
})),
|
|
1405
|
+
resource_templates: templates.map((template) => ({
|
|
1406
|
+
uriTemplate: template.uriTemplate,
|
|
1407
|
+
name: template.name,
|
|
1408
|
+
description: template.description,
|
|
1409
|
+
mimeType: template.mimeType
|
|
1410
|
+
})),
|
|
1411
|
+
prompts: prompts.map((prompt) => ({
|
|
1412
|
+
name: prompt.name,
|
|
1413
|
+
description: prompt.description,
|
|
1414
|
+
arguments: prompt.arguments
|
|
1415
|
+
}))
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
// === Internal helpers used by connect() and loadMcp() ===
|
|
1419
|
+
buildUpstreamConfig(mcpName2, transport) {
|
|
1420
|
+
return {
|
|
1421
|
+
transport,
|
|
1422
|
+
onTransportError: (error) => {
|
|
1423
|
+
this.config.onTransportError?.(mcpName2, error);
|
|
1424
|
+
},
|
|
1425
|
+
notifications: {
|
|
1426
|
+
onToolsListChanged: () => this.forwarder.handleToolsListChanged(mcpName2),
|
|
1427
|
+
onResourcesListChanged: () => this.forwarder.handleResourcesListChanged(mcpName2),
|
|
1428
|
+
onResourceUpdated: (params) => this.forwarder.handleResourceUpdated(params),
|
|
1429
|
+
onPromptsListChanged: () => this.forwarder.handlePromptsListChanged(mcpName2),
|
|
1430
|
+
onLogMessage: (params) => this.forwarder.handleLogMessage(mcpName2, params)
|
|
1431
|
+
},
|
|
1432
|
+
serverRequests: {
|
|
1433
|
+
onCreateMessage: (params, opts) => this.forwardCreateMessage(params, opts),
|
|
1434
|
+
onElicitInput: (params, opts) => this.forwardElicitInput(params, opts),
|
|
1435
|
+
onListRoots: (params, opts) => this.forwardListRoots(params, opts)
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Rebuilds the `ToolCatalog` from current state. Called whenever `toolsByMcp` or the
|
|
1441
|
+
* lazy-registry membership changes — including initial connect, upstream-emitted
|
|
1442
|
+
* `tools/list_changed`, and successful `loadMcp`.
|
|
1443
|
+
*/
|
|
1444
|
+
rebuildToolCatalog() {
|
|
1445
|
+
if (this.config.namespaced) {
|
|
1446
|
+
this.toolCatalog = ToolCatalog.fromGroupedWithLazy(
|
|
1447
|
+
this.toolsByMcp,
|
|
1448
|
+
this.lazyRegistry.descriptions()
|
|
1449
|
+
);
|
|
1450
|
+
} else {
|
|
1451
|
+
this.toolCatalog = ToolCatalog.fromFlat([...this.toolsByMcp.values()][0] ?? []);
|
|
1452
|
+
}
|
|
1100
1453
|
}
|
|
1101
1454
|
get catalog() {
|
|
1102
1455
|
if (this.toolCatalog === null) {
|
|
@@ -1285,7 +1638,9 @@ var import_types2 = require("@modelcontextprotocol/sdk/types.js");
|
|
|
1285
1638
|
var import_zod3 = require("zod");
|
|
1286
1639
|
var DISCOVER_TOOL_NAME = "discover_tool";
|
|
1287
1640
|
var USE_TOOL_NAME = "use_tool";
|
|
1641
|
+
var LOAD_MCP_NAME = "load_mcp";
|
|
1288
1642
|
var USE_TOOL_DESCRIPTION = "Use a tool that was previously discovered with the discover_tool tool.";
|
|
1643
|
+
var LOAD_MCP_DESCRIPTION = "Load a previously-deferred MCP server so that its tools, resources, and prompts become available. Pass the server name as shown in the <mcp_servers> block of the discover_tool description. Loading is permanent for the remainder of this session.";
|
|
1289
1644
|
var DISCOVER_TOOL_INPUT_SCHEMA = {
|
|
1290
1645
|
type: "object",
|
|
1291
1646
|
properties: {
|
|
@@ -1301,11 +1656,19 @@ var USE_TOOL_INPUT_SCHEMA = {
|
|
|
1301
1656
|
},
|
|
1302
1657
|
required: ["tool_name"]
|
|
1303
1658
|
};
|
|
1659
|
+
var LOAD_MCP_INPUT_SCHEMA = {
|
|
1660
|
+
type: "object",
|
|
1661
|
+
properties: {
|
|
1662
|
+
mcp_name: { type: "string" }
|
|
1663
|
+
},
|
|
1664
|
+
required: ["mcp_name"]
|
|
1665
|
+
};
|
|
1304
1666
|
var DiscoverToolArgsSchema = import_zod3.z.object({ tool_name: import_zod3.z.string() });
|
|
1305
1667
|
var UseToolArgsSchema = import_zod3.z.object({
|
|
1306
1668
|
tool_name: import_zod3.z.string(),
|
|
1307
1669
|
tool_input: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()).default({})
|
|
1308
1670
|
});
|
|
1671
|
+
var LoadMcpArgsSchema = import_zod3.z.object({ mcp_name: import_zod3.z.string() });
|
|
1309
1672
|
var ProxyServer = class {
|
|
1310
1673
|
catalog;
|
|
1311
1674
|
callTool;
|
|
@@ -1315,6 +1678,7 @@ var ProxyServer = class {
|
|
|
1315
1678
|
complete;
|
|
1316
1679
|
setLoggingLevelCallback;
|
|
1317
1680
|
onRootsListChangedCallback;
|
|
1681
|
+
loadMcpCallback;
|
|
1318
1682
|
sdkServer = null;
|
|
1319
1683
|
constructor({
|
|
1320
1684
|
catalog,
|
|
@@ -1324,7 +1688,8 @@ var ProxyServer = class {
|
|
|
1324
1688
|
prompts,
|
|
1325
1689
|
complete,
|
|
1326
1690
|
setLoggingLevel,
|
|
1327
|
-
onRootsListChanged
|
|
1691
|
+
onRootsListChanged,
|
|
1692
|
+
loadMcp
|
|
1328
1693
|
}) {
|
|
1329
1694
|
this.catalog = catalog;
|
|
1330
1695
|
this.callTool = callTool;
|
|
@@ -1334,6 +1699,7 @@ var ProxyServer = class {
|
|
|
1334
1699
|
this.complete = complete;
|
|
1335
1700
|
this.setLoggingLevelCallback = setLoggingLevel;
|
|
1336
1701
|
this.onRootsListChangedCallback = onRootsListChanged;
|
|
1702
|
+
this.loadMcpCallback = loadMcp;
|
|
1337
1703
|
}
|
|
1338
1704
|
buildServer() {
|
|
1339
1705
|
const server = new import_server.Server(
|
|
@@ -1466,8 +1832,8 @@ var ProxyServer = class {
|
|
|
1466
1832
|
return options;
|
|
1467
1833
|
}
|
|
1468
1834
|
registerToolHandlers(server) {
|
|
1469
|
-
server.setRequestHandler(import_types2.ListToolsRequestSchema, async () =>
|
|
1470
|
-
tools
|
|
1835
|
+
server.setRequestHandler(import_types2.ListToolsRequestSchema, async () => {
|
|
1836
|
+
const tools = [
|
|
1471
1837
|
{
|
|
1472
1838
|
name: DISCOVER_TOOL_NAME,
|
|
1473
1839
|
description: this.catalog().discoverToolDescription,
|
|
@@ -1478,8 +1844,16 @@ var ProxyServer = class {
|
|
|
1478
1844
|
description: USE_TOOL_DESCRIPTION,
|
|
1479
1845
|
inputSchema: USE_TOOL_INPUT_SCHEMA
|
|
1480
1846
|
}
|
|
1481
|
-
]
|
|
1482
|
-
|
|
1847
|
+
];
|
|
1848
|
+
if (this.loadMcpCallback !== void 0) {
|
|
1849
|
+
tools.push({
|
|
1850
|
+
name: LOAD_MCP_NAME,
|
|
1851
|
+
description: LOAD_MCP_DESCRIPTION,
|
|
1852
|
+
inputSchema: LOAD_MCP_INPUT_SCHEMA
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
return { tools };
|
|
1856
|
+
});
|
|
1483
1857
|
server.setRequestHandler(
|
|
1484
1858
|
import_types2.CallToolRequestSchema,
|
|
1485
1859
|
async (request, extra) => {
|
|
@@ -1504,6 +1878,25 @@ var ProxyServer = class {
|
|
|
1504
1878
|
this.buildCallOptions(request, extra)
|
|
1505
1879
|
);
|
|
1506
1880
|
}
|
|
1881
|
+
if (name === LOAD_MCP_NAME && this.loadMcpCallback !== void 0) {
|
|
1882
|
+
const args = LoadMcpArgsSchema.parse(rawArgs ?? {});
|
|
1883
|
+
try {
|
|
1884
|
+
const result = await this.loadMcpCallback(args.mcp_name);
|
|
1885
|
+
return {
|
|
1886
|
+
// The structured payload is JSON-serialized into a text block. We also
|
|
1887
|
+
// populate `structuredContent` so MCP clients that prefer typed data can
|
|
1888
|
+
// consume the same response without parsing the text body.
|
|
1889
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1890
|
+
structuredContent: result
|
|
1891
|
+
};
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1894
|
+
return {
|
|
1895
|
+
isError: true,
|
|
1896
|
+
content: [{ type: "text", text: message }]
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1507
1900
|
return {
|
|
1508
1901
|
isError: true,
|
|
1509
1902
|
content: [{ type: "text", text: `Unknown tool: "${name}"` }]
|
|
@@ -1618,9 +2011,9 @@ function createTransport(config) {
|
|
|
1618
2011
|
var SINGLE_MCP_NAME = "__default__";
|
|
1619
2012
|
async function startProxy(command, args) {
|
|
1620
2013
|
const transport = new import_stdio3.StdioClientTransport({ command, args });
|
|
1621
|
-
const
|
|
2014
|
+
const eagerMcps = /* @__PURE__ */ new Map([[SINGLE_MCP_NAME, { transport }]]);
|
|
1622
2015
|
const orchestrator = buildOrchestrator({
|
|
1623
|
-
|
|
2016
|
+
eagerMcps,
|
|
1624
2017
|
namespaced: false,
|
|
1625
2018
|
transportErrorPrefix: () => "Upstream MCP"
|
|
1626
2019
|
});
|
|
@@ -1628,12 +2021,19 @@ async function startProxy(command, args) {
|
|
|
1628
2021
|
}
|
|
1629
2022
|
async function startProxyFromConfig(options = {}) {
|
|
1630
2023
|
const config = loadConfig(options);
|
|
1631
|
-
const
|
|
2024
|
+
const eagerMcps = /* @__PURE__ */ new Map();
|
|
2025
|
+
const lazyMcps = /* @__PURE__ */ new Map();
|
|
1632
2026
|
for (const [name, entry] of Object.entries(config.mcp)) {
|
|
1633
|
-
|
|
2027
|
+
const transport = createTransport(entry);
|
|
2028
|
+
if (entry.description !== void 0) {
|
|
2029
|
+
lazyMcps.set(name, { transport, description: entry.description });
|
|
2030
|
+
} else {
|
|
2031
|
+
eagerMcps.set(name, { transport });
|
|
2032
|
+
}
|
|
1634
2033
|
}
|
|
1635
2034
|
const orchestrator = buildOrchestrator({
|
|
1636
|
-
|
|
2035
|
+
eagerMcps,
|
|
2036
|
+
lazyMcps,
|
|
1637
2037
|
namespaced: true,
|
|
1638
2038
|
transportErrorPrefix: (mcpName2) => `Upstream MCP "${mcpName2}"`
|
|
1639
2039
|
});
|
|
@@ -1642,7 +2042,8 @@ async function startProxyFromConfig(options = {}) {
|
|
|
1642
2042
|
var activeShutdown = { shutdown: null };
|
|
1643
2043
|
function buildOrchestrator(params) {
|
|
1644
2044
|
return new Orchestrator({
|
|
1645
|
-
|
|
2045
|
+
eagerMcps: params.eagerMcps,
|
|
2046
|
+
lazyMcps: params.lazyMcps,
|
|
1646
2047
|
namespaced: params.namespaced,
|
|
1647
2048
|
onTransportError: (mcpName2, error) => {
|
|
1648
2049
|
import_node_process6.default.stderr.write(
|
|
@@ -1691,7 +2092,10 @@ async function runProxy(orchestrator) {
|
|
|
1691
2092
|
} : void 0,
|
|
1692
2093
|
complete: orchestrator.capabilities.completions !== void 0 ? (params, options) => orchestrator.complete(params, options) : void 0,
|
|
1693
2094
|
setLoggingLevel: orchestrator.capabilities.logging !== void 0 ? (level, options) => orchestrator.setLoggingLevel(level, options) : void 0,
|
|
1694
|
-
onRootsListChanged: () => orchestrator.broadcastRootsListChanged()
|
|
2095
|
+
onRootsListChanged: () => orchestrator.broadcastRootsListChanged(),
|
|
2096
|
+
// Only register the `load_mcp` meta-tool when dynamic discovery is enabled —
|
|
2097
|
+
// i.e. when the config declared at least one lazy upstream MCP.
|
|
2098
|
+
loadMcp: orchestrator.hasDynamicDiscovery ? (mcpName2) => orchestrator.loadMcp(mcpName2) : void 0
|
|
1695
2099
|
});
|
|
1696
2100
|
orchestrator.setNotificationHandlers({
|
|
1697
2101
|
onToolsListChanged: () => proxyServer.sendToolListChanged(),
|