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