maestro-agent-sdk 0.1.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/LICENSE +21 -0
- package/NOTICE +24 -0
- package/README.md +133 -0
- package/dist/agents/contracts.d.ts +49 -0
- package/dist/agents/contracts.d.ts.map +1 -0
- package/dist/agents/contracts.js +2 -0
- package/dist/agents/contracts.js.map +1 -0
- package/dist/agents/rollout/shared.d.ts +24 -0
- package/dist/agents/rollout/shared.d.ts.map +1 -0
- package/dist/agents/rollout/shared.js +105 -0
- package/dist/agents/rollout/shared.js.map +1 -0
- package/dist/core/agent.d.ts +71 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +22 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/loop.d.ts +26 -0
- package/dist/core/loop.d.ts.map +1 -0
- package/dist/core/loop.js +317 -0
- package/dist/core/loop.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/client.d.ts +79 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +176 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/pool-cache.d.ts +103 -0
- package/dist/mcp/pool-cache.d.ts.map +1 -0
- package/dist/mcp/pool-cache.js +249 -0
- package/dist/mcp/pool-cache.js.map +1 -0
- package/dist/mcp/pool.d.ts +65 -0
- package/dist/mcp/pool.d.ts.map +1 -0
- package/dist/mcp/pool.js +86 -0
- package/dist/mcp/pool.js.map +1 -0
- package/dist/media/file-events.d.ts +8 -0
- package/dist/media/file-events.d.ts.map +1 -0
- package/dist/media/file-events.js +15 -0
- package/dist/media/file-events.js.map +1 -0
- package/dist/memory/active-task-template.d.ts +34 -0
- package/dist/memory/active-task-template.d.ts.map +1 -0
- package/dist/memory/active-task-template.js +63 -0
- package/dist/memory/active-task-template.js.map +1 -0
- package/dist/memory/compressor.d.ts +87 -0
- package/dist/memory/compressor.d.ts.map +1 -0
- package/dist/memory/compressor.js +164 -0
- package/dist/memory/compressor.js.map +1 -0
- package/dist/memory/hash.d.ts +17 -0
- package/dist/memory/hash.d.ts.map +1 -0
- package/dist/memory/hash.js +20 -0
- package/dist/memory/hash.js.map +1 -0
- package/dist/memory/prune.d.ts +117 -0
- package/dist/memory/prune.d.ts.map +1 -0
- package/dist/memory/prune.js +416 -0
- package/dist/memory/prune.js.map +1 -0
- package/dist/memory/reminder.d.ts +57 -0
- package/dist/memory/reminder.d.ts.map +1 -0
- package/dist/memory/reminder.js +57 -0
- package/dist/memory/reminder.js.map +1 -0
- package/dist/memory/scrubber.d.ts +28 -0
- package/dist/memory/scrubber.d.ts.map +1 -0
- package/dist/memory/scrubber.js +147 -0
- package/dist/memory/scrubber.js.map +1 -0
- package/dist/memory/token-estimate.d.ts +10 -0
- package/dist/memory/token-estimate.d.ts.map +1 -0
- package/dist/memory/token-estimate.js +69 -0
- package/dist/memory/token-estimate.js.map +1 -0
- package/dist/platform/config.d.ts +12 -0
- package/dist/platform/config.d.ts.map +1 -0
- package/dist/platform/config.js +54 -0
- package/dist/platform/config.js.map +1 -0
- package/dist/platform/jsonl.d.ts +15 -0
- package/dist/platform/jsonl.d.ts.map +1 -0
- package/dist/platform/jsonl.js +80 -0
- package/dist/platform/jsonl.js.map +1 -0
- package/dist/platform/lifecycle.d.ts +22 -0
- package/dist/platform/lifecycle.d.ts.map +1 -0
- package/dist/platform/lifecycle.js +60 -0
- package/dist/platform/lifecycle.js.map +1 -0
- package/dist/platform/logger.d.ts +26 -0
- package/dist/platform/logger.d.ts.map +1 -0
- package/dist/platform/logger.js +41 -0
- package/dist/platform/logger.js.map +1 -0
- package/dist/platform/mcp-config.d.ts +15 -0
- package/dist/platform/mcp-config.d.ts.map +1 -0
- package/dist/platform/mcp-config.js +8 -0
- package/dist/platform/mcp-config.js.map +1 -0
- package/dist/provider.d.ts +81 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +444 -0
- package/dist/provider.js.map +1 -0
- package/dist/providers/anthropic.d.ts +132 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +518 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/base.d.ts +140 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +2 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/deepseek.d.ts +118 -0
- package/dist/providers/deepseek.d.ts.map +1 -0
- package/dist/providers/deepseek.js +467 -0
- package/dist/providers/deepseek.js.map +1 -0
- package/dist/registry.d.ts +3 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +94 -0
- package/dist/registry.js.map +1 -0
- package/dist/session-store.d.ts +133 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +277 -0
- package/dist/session-store.js.map +1 -0
- package/dist/skills/curator.d.ts +104 -0
- package/dist/skills/curator.d.ts.map +1 -0
- package/dist/skills/curator.js +162 -0
- package/dist/skills/curator.js.map +1 -0
- package/dist/skills/index-builder.d.ts +42 -0
- package/dist/skills/index-builder.d.ts.map +1 -0
- package/dist/skills/index-builder.js +94 -0
- package/dist/skills/index-builder.js.map +1 -0
- package/dist/skills/loader.d.ts +107 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +286 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/preprocess.d.ts +45 -0
- package/dist/skills/preprocess.d.ts.map +1 -0
- package/dist/skills/preprocess.js +126 -0
- package/dist/skills/preprocess.js.map +1 -0
- package/dist/skills/usage.d.ts +75 -0
- package/dist/skills/usage.d.ts.map +1 -0
- package/dist/skills/usage.js +147 -0
- package/dist/skills/usage.js.map +1 -0
- package/dist/state/todos.d.ts +95 -0
- package/dist/state/todos.d.ts.map +1 -0
- package/dist/state/todos.js +198 -0
- package/dist/state/todos.js.map +1 -0
- package/dist/storage/conversations.d.ts +28 -0
- package/dist/storage/conversations.d.ts.map +1 -0
- package/dist/storage/conversations.js +8 -0
- package/dist/storage/conversations.js.map +1 -0
- package/dist/sub-agent/runner.d.ts +78 -0
- package/dist/sub-agent/runner.d.ts.map +1 -0
- package/dist/sub-agent/runner.js +215 -0
- package/dist/sub-agent/runner.js.map +1 -0
- package/dist/tools/builtin/agent.d.ts +33 -0
- package/dist/tools/builtin/agent.d.ts.map +1 -0
- package/dist/tools/builtin/agent.js +76 -0
- package/dist/tools/builtin/agent.js.map +1 -0
- package/dist/tools/builtin/bash.d.ts +11 -0
- package/dist/tools/builtin/bash.d.ts.map +1 -0
- package/dist/tools/builtin/bash.js +91 -0
- package/dist/tools/builtin/bash.js.map +1 -0
- package/dist/tools/builtin/edit.d.ts +21 -0
- package/dist/tools/builtin/edit.d.ts.map +1 -0
- package/dist/tools/builtin/edit.js +238 -0
- package/dist/tools/builtin/edit.js.map +1 -0
- package/dist/tools/builtin/read.d.ts +17 -0
- package/dist/tools/builtin/read.d.ts.map +1 -0
- package/dist/tools/builtin/read.js +139 -0
- package/dist/tools/builtin/read.js.map +1 -0
- package/dist/tools/builtin/sandbox.d.ts +16 -0
- package/dist/tools/builtin/sandbox.d.ts.map +1 -0
- package/dist/tools/builtin/sandbox.js +58 -0
- package/dist/tools/builtin/sandbox.js.map +1 -0
- package/dist/tools/builtin/skill_view.d.ts +37 -0
- package/dist/tools/builtin/skill_view.d.ts.map +1 -0
- package/dist/tools/builtin/skill_view.js +82 -0
- package/dist/tools/builtin/skill_view.js.map +1 -0
- package/dist/tools/builtin/todo_write.d.ts +29 -0
- package/dist/tools/builtin/todo_write.d.ts.map +1 -0
- package/dist/tools/builtin/todo_write.js +96 -0
- package/dist/tools/builtin/todo_write.js.map +1 -0
- package/dist/tools/builtin/web_fetch.d.ts +10 -0
- package/dist/tools/builtin/web_fetch.d.ts.map +1 -0
- package/dist/tools/builtin/web_fetch.js +150 -0
- package/dist/tools/builtin/web_fetch.js.map +1 -0
- package/dist/tools/builtin/write.d.ts +35 -0
- package/dist/tools/builtin/write.d.ts.map +1 -0
- package/dist/tools/builtin/write.js +70 -0
- package/dist/tools/builtin/write.js.map +1 -0
- package/dist/tools/file-state.d.ts +99 -0
- package/dist/tools/file-state.d.ts.map +1 -0
- package/dist/tools/file-state.js +133 -0
- package/dist/tools/file-state.js.map +1 -0
- package/dist/tools/hooks/sandbox-fs.d.ts +25 -0
- package/dist/tools/hooks/sandbox-fs.d.ts.map +1 -0
- package/dist/tools/hooks/sandbox-fs.js +48 -0
- package/dist/tools/hooks/sandbox-fs.js.map +1 -0
- package/dist/tools/registry.d.ts +102 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +93 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/types.d.ts +109 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +20 -0
- package/dist/types.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
3
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4
|
+
import { logger } from "../platform/logger.js";
|
|
5
|
+
export class MaestroMcpClient {
|
|
6
|
+
name;
|
|
7
|
+
spec;
|
|
8
|
+
transportOverride;
|
|
9
|
+
client;
|
|
10
|
+
transport;
|
|
11
|
+
started = false;
|
|
12
|
+
constructor(name, spec,
|
|
13
|
+
/**
|
|
14
|
+
* Test-only override: when supplied, `start()` connects to this transport
|
|
15
|
+
* instead of building one from `spec`. Production callers pass two args.
|
|
16
|
+
*/
|
|
17
|
+
transportOverride) {
|
|
18
|
+
this.name = name;
|
|
19
|
+
this.spec = spec;
|
|
20
|
+
this.transportOverride = transportOverride;
|
|
21
|
+
this.client = new Client({ name: "clawgram-maestro", version: "0.1.0" }, { capabilities: {} });
|
|
22
|
+
}
|
|
23
|
+
async start() {
|
|
24
|
+
if (this.started)
|
|
25
|
+
return;
|
|
26
|
+
this.transport = this.transportOverride ?? this.buildTransport();
|
|
27
|
+
await this.client.connect(this.transport);
|
|
28
|
+
this.started = true;
|
|
29
|
+
}
|
|
30
|
+
/** Fetch `tools/list` and convert to Maestro/Anthropic shape with prefixed names. */
|
|
31
|
+
async listTools() {
|
|
32
|
+
const res = await this.client.listTools();
|
|
33
|
+
const tools = Array.isArray(res.tools) ? res.tools : [];
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const t of tools) {
|
|
36
|
+
const original = String(t.name ?? "");
|
|
37
|
+
if (!original)
|
|
38
|
+
continue;
|
|
39
|
+
const publicName = makePublicName(this.name, original);
|
|
40
|
+
out.push({
|
|
41
|
+
publicName,
|
|
42
|
+
originalName: original,
|
|
43
|
+
serverName: this.name,
|
|
44
|
+
schema: {
|
|
45
|
+
name: publicName,
|
|
46
|
+
description: typeof t.description === "string" ? t.description : "",
|
|
47
|
+
input_schema: normalizeInputSchema(t.inputSchema),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Invoke an MCP tool. Returns the FULL rendered payload string for the
|
|
55
|
+
* Maestro tool_result block — the model needs to see complete tool output on
|
|
56
|
+
* the next turn, matching how claude/codex SDKs feed full payloads back.
|
|
57
|
+
* The 200-char cap that claude-provider.ts:373 and codex-provider.ts apply
|
|
58
|
+
* is a UnifiedEvent display concern only; Maestro enforces it inside the
|
|
59
|
+
* agent loop (`loop.ts:TOOL_RESULT_PREVIEW_MAX`) at the emit site, not here.
|
|
60
|
+
*
|
|
61
|
+
* Optional `abortSignal` propagates the Maestro-level abort down into the
|
|
62
|
+
* SDK's JSON-RPC request, so an in-flight `tools/call` is cancelled instead
|
|
63
|
+
* of left blocking on a dead server.
|
|
64
|
+
*/
|
|
65
|
+
async callTool(originalName, input, abortSignal) {
|
|
66
|
+
const res = await this.client.callTool({ name: originalName, arguments: input }, undefined, abortSignal ? { signal: abortSignal } : undefined);
|
|
67
|
+
return renderCallResult(res);
|
|
68
|
+
}
|
|
69
|
+
async close() {
|
|
70
|
+
if (!this.started)
|
|
71
|
+
return;
|
|
72
|
+
this.started = false;
|
|
73
|
+
try {
|
|
74
|
+
await this.client.close();
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
logger.warn({ err, server: this.name }, "maestro mcp client: close failed (continuing)");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
buildTransport() {
|
|
81
|
+
if (this.spec.type === "sse") {
|
|
82
|
+
if (!this.spec.url) {
|
|
83
|
+
throw new Error(`Maestro MCP '${this.name}': sse spec missing url`);
|
|
84
|
+
}
|
|
85
|
+
// Use the URL exactly as supplied — same posture as claude/codex SDKs.
|
|
86
|
+
// We used to rewrite `localhost` → `127.0.0.1` here as a workaround for
|
|
87
|
+
// a playwright-mcp that bound IPv4-only while Node `eventsource`
|
|
88
|
+
// resolved `::1` first; the playwright spawn args now pin `--host
|
|
89
|
+
// 127.0.0.1` so every transport reaches the same address family
|
|
90
|
+
// without per-client hacks, and a future remote-MCP URL won't get
|
|
91
|
+
// silently rewritten under us.
|
|
92
|
+
return new SSEClientTransport(new URL(this.spec.url));
|
|
93
|
+
}
|
|
94
|
+
if (!this.spec.command) {
|
|
95
|
+
throw new Error(`Maestro MCP '${this.name}': no command and no sse url`);
|
|
96
|
+
}
|
|
97
|
+
return new StdioClientTransport({
|
|
98
|
+
command: this.spec.command,
|
|
99
|
+
args: this.spec.args ?? [],
|
|
100
|
+
env: filteredEnv(this.spec.env),
|
|
101
|
+
stderr: "inherit",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Public MCP tool name. Mirrors Claude SDK's `mcp__<server>__<tool>` convention
|
|
107
|
+
* so the model sees consistent names across providers in the same topic.
|
|
108
|
+
*/
|
|
109
|
+
export function makePublicName(server, tool) {
|
|
110
|
+
return `mcp__${sanitizeName(server)}__${sanitizeName(tool)}`;
|
|
111
|
+
}
|
|
112
|
+
/** Allowed by Anthropic tool name regex: `^[a-zA-Z0-9_-]+$`. */
|
|
113
|
+
function sanitizeName(s) {
|
|
114
|
+
return s.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
115
|
+
}
|
|
116
|
+
function normalizeInputSchema(s) {
|
|
117
|
+
if (s && typeof s === "object") {
|
|
118
|
+
const obj = s;
|
|
119
|
+
const props = obj.properties && typeof obj.properties === "object"
|
|
120
|
+
? obj.properties
|
|
121
|
+
: {};
|
|
122
|
+
const required = Array.isArray(obj.required) ? obj.required : undefined;
|
|
123
|
+
return {
|
|
124
|
+
type: "object",
|
|
125
|
+
properties: props,
|
|
126
|
+
...(required ? { required } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { type: "object", properties: {} };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Render an MCP `callTool` result into a full payload string for the Maestro
|
|
133
|
+
* tool_result block. No length cap here — the model must see the complete
|
|
134
|
+
* output to act on it (e.g. accessibility trees from playwright, OCR text).
|
|
135
|
+
* The 200-char display cap lives in `loop.ts` where the UnifiedEvent is
|
|
136
|
+
* surfaced, mirroring claude/codex (their SDK feeds full payloads to the
|
|
137
|
+
* model and only the UnifiedEvent emit path truncates for downstream
|
|
138
|
+
* telegram-render layout).
|
|
139
|
+
*/
|
|
140
|
+
function renderCallResult(res) {
|
|
141
|
+
const r = res;
|
|
142
|
+
const blocks = Array.isArray(r.content) ? r.content : [];
|
|
143
|
+
const text = blocks
|
|
144
|
+
.map((b) => {
|
|
145
|
+
if (b && typeof b === "object" && "type" in b && b.type === "text") {
|
|
146
|
+
const t = b.text;
|
|
147
|
+
return typeof t === "string" ? t : "";
|
|
148
|
+
}
|
|
149
|
+
return "";
|
|
150
|
+
})
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.join("\n");
|
|
153
|
+
if (r.isError) {
|
|
154
|
+
return JSON.stringify({ error: text || "mcp tool error" });
|
|
155
|
+
}
|
|
156
|
+
if (text)
|
|
157
|
+
return text;
|
|
158
|
+
// Non-text content (image / structured): pass JSON dump so the model still has signal.
|
|
159
|
+
return JSON.stringify(r.content ?? null);
|
|
160
|
+
}
|
|
161
|
+
/** StdioClientTransport requires `Record<string, string>`; drop undefined values from process.env. */
|
|
162
|
+
function filteredEnv(extra) {
|
|
163
|
+
const out = {};
|
|
164
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
165
|
+
if (typeof v === "string")
|
|
166
|
+
out[k] = v;
|
|
167
|
+
}
|
|
168
|
+
if (extra) {
|
|
169
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
170
|
+
if (typeof v === "string")
|
|
171
|
+
out[k] = v;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/mcp/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { MaestroMcpClient, type MaestroMcpServerSpec } from "../mcp/client.js";
|
|
2
|
+
/**
|
|
3
|
+
* MCP client cache — process-wide pool of started `MaestroMcpClient`s keyed on
|
|
4
|
+
* (userId, session, groupId, agentKind, serverName, specHash).
|
|
5
|
+
*
|
|
6
|
+
* Motivation: every `maestroProvider` invocation previously called
|
|
7
|
+
* `startMcpPool` which spawned every configured MCP server fresh. Most servers
|
|
8
|
+
* are stdio bun subprocesses (send-file, ocr, wiki, session-comm, etc.) and
|
|
9
|
+
* each one costs ~80–200ms to spawn + handshake. With 8–10 servers per turn
|
|
10
|
+
* that's ~1s of pure spawn latency before the model even starts thinking.
|
|
11
|
+
*
|
|
12
|
+
* Phase 1 caching reuses the same `MaestroMcpClient` across turns within the
|
|
13
|
+
* same session — the MCP `Client` connection stays open, only the
|
|
14
|
+
* `registerMcpTools` wiring is rebuilt per turn (cheap, just a Map fill).
|
|
15
|
+
*
|
|
16
|
+
* Lifecycle invariants:
|
|
17
|
+
* - One physical `MaestroMcpClient.start()` per cache key.
|
|
18
|
+
* - Per-turn callers acquire via `getOrStartClient` (refcount++) and release
|
|
19
|
+
* via `releaseClient` (refcount--). Eviction never closes a client whose
|
|
20
|
+
* refcount > 0 — an abort that closes the pool only drops the lease.
|
|
21
|
+
* - Idle TTL (`MAESTRO_MCP_POOL_IDLE_TTL_MS`) and LRU cap
|
|
22
|
+
* (`MAESTRO_MCP_POOL_MAX`) evict clients whose refcount is 0.
|
|
23
|
+
* - One process-wide `beforeExit`/`SIGINT`/`SIGTERM` handler closes every
|
|
24
|
+
* cached client on graceful shutdown so MCP subprocesses don't outlive us.
|
|
25
|
+
*
|
|
26
|
+
* Negative caching is off in Phase 1 — a failed `start()` is not cached, so
|
|
27
|
+
* the next turn retries fresh. Means a server that's transiently down doesn't
|
|
28
|
+
* stay marked-bad past one turn, at the cost of paying its spawn-fail latency
|
|
29
|
+
* again next time.
|
|
30
|
+
*/
|
|
31
|
+
/** Idle TTL — clients with refcount 0 evicted after this many ms. */
|
|
32
|
+
export declare const MAESTRO_MCP_POOL_IDLE_TTL_MS: number;
|
|
33
|
+
/** Hard cap on cached clients. LRU eviction past this. */
|
|
34
|
+
export declare const MAESTRO_MCP_POOL_MAX: number;
|
|
35
|
+
/** Context that scopes a cache key. Two turns with the same context + server
|
|
36
|
+
* + spec share the same client; otherwise they get separate slots. */
|
|
37
|
+
export interface CacheKeyContext {
|
|
38
|
+
/** Telegram user id as string. Different users never share clients. */
|
|
39
|
+
userId?: string;
|
|
40
|
+
/** Topic/session name. */
|
|
41
|
+
session?: string;
|
|
42
|
+
/** Forum group id (manager-scope servers vary by group). */
|
|
43
|
+
groupId?: number;
|
|
44
|
+
/** Agent kind — currently always "maestro" but reserved so a future
|
|
45
|
+
* cross-agent MCP cache won't collide. */
|
|
46
|
+
agentKind?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Client factory — overridable in tests to inject an InMemoryTransport-backed
|
|
49
|
+
* `MaestroMcpClient` instead of spawning a real stdio/sse subprocess. */
|
|
50
|
+
type ClientFactory = (name: string, spec: MaestroMcpServerSpec) => MaestroMcpClient;
|
|
51
|
+
/**
|
|
52
|
+
* Build a stable hash of a server spec. We hash command + args + transport
|
|
53
|
+
* url + sorted env keys (values excluded — they often contain per-turn
|
|
54
|
+
* variables like depth, group ids; would defeat caching).
|
|
55
|
+
*
|
|
56
|
+
* Two callers with the same hash get the same client back — caller is
|
|
57
|
+
* responsible for ensuring that's semantically correct (i.e. they pass the
|
|
58
|
+
* same `CacheKeyContext` if their spec encodes the context).
|
|
59
|
+
*/
|
|
60
|
+
export declare function hashSpec(spec: MaestroMcpServerSpec): string;
|
|
61
|
+
/**
|
|
62
|
+
* Acquire a started `MaestroMcpClient` for (ctx, serverName, spec). If a fresh
|
|
63
|
+
* spawn is needed it happens inside this call; the returned client is already
|
|
64
|
+
* past `start()`.
|
|
65
|
+
*
|
|
66
|
+
* Refcount semantics: every successful call increments refcount by 1 and the
|
|
67
|
+
* caller MUST pair it with `releaseClient` (typically in a finally block, or
|
|
68
|
+
* inside the pool object's close()).
|
|
69
|
+
*/
|
|
70
|
+
export declare function getOrStartClient(ctx: CacheKeyContext, serverName: string, spec: MaestroMcpServerSpec): Promise<MaestroMcpClient>;
|
|
71
|
+
/**
|
|
72
|
+
* Decrement refcount on a previously-acquired client. Does not close anything;
|
|
73
|
+
* the sweeper / cap-evictor closes idle clients later.
|
|
74
|
+
*
|
|
75
|
+
* `clientRef` should be the exact instance returned by `getOrStartClient`.
|
|
76
|
+
* Releasing a client we don't track is a no-op + warn (defensive against
|
|
77
|
+
* double-release).
|
|
78
|
+
*/
|
|
79
|
+
export declare function releaseClient(clientRef: MaestroMcpClient): void;
|
|
80
|
+
/** Run one idle-TTL sweep. Exported for tests. */
|
|
81
|
+
export declare function sweepIdle(now?: number): Promise<number>;
|
|
82
|
+
/** Stop the sweeper. Used by tests to reset state. */
|
|
83
|
+
export declare function stopSweeper(): void;
|
|
84
|
+
/**
|
|
85
|
+
* Close every cached client. Awaits up to `timeoutMs` total (3s default).
|
|
86
|
+
* Safe to call multiple times — entries are removed as they close.
|
|
87
|
+
*/
|
|
88
|
+
export declare function closeAll(timeoutMs?: number): Promise<void>;
|
|
89
|
+
/** Test-only: drop all entries without closing. Used to reset state between
|
|
90
|
+
* test runs that share the module singleton. */
|
|
91
|
+
export declare function __resetForTests(): void;
|
|
92
|
+
/** Test-only: swap the client factory. Used to inject a fake client backed by
|
|
93
|
+
* `InMemoryTransport` so tests don't spawn real MCP subprocesses. */
|
|
94
|
+
export declare function __setClientFactoryForTests(factory: ClientFactory): void;
|
|
95
|
+
/** Test-only: read cache size. */
|
|
96
|
+
export declare function __cacheSize(): number;
|
|
97
|
+
/** Test-only: read entry by key parts. */
|
|
98
|
+
export declare function __getEntry(ctx: CacheKeyContext, serverName: string, spec: MaestroMcpServerSpec): {
|
|
99
|
+
refcount: number;
|
|
100
|
+
lastUsed: number;
|
|
101
|
+
} | null;
|
|
102
|
+
export {};
|
|
103
|
+
//# sourceMappingURL=pool-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool-cache.d.ts","sourceRoot":"","sources":["../../src/mcp/pool-cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAI3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,qEAAqE;AACrE,eAAO,MAAM,4BAA4B,QAGxC,CAAC;AAEF,0DAA0D;AAC1D,eAAO,MAAM,oBAAoB,QAAgE,CAAC;AAElG;uEACuE;AACvE,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0BAA0B;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;+CAC2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAcD;0EAC0E;AAC1E,KAAK,aAAa,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,KAAK,gBAAgB,CAAC;AAGpF;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,oBAAoB,GAAG,MAAM,CAW3D;AAaD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,eAAe,EACpB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CAyC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,gBAAgB,GAAG,IAAI,CAc/D;AA2BD,kDAAkD;AAClD,wBAAsB,SAAS,CAAC,GAAG,GAAE,MAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CASzE;AAeD,sDAAsD;AACtD,wBAAgB,WAAW,IAAI,IAAI,CAKlC;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,SAAS,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAiB9D;AAYD;iDACiD;AACjD,wBAAgB,eAAe,IAAI,IAAI,CAKtC;AAED;sEACsE;AACtE,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAEvE;AAED,kCAAkC;AAClC,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,0CAA0C;AAC1C,wBAAgB,UAAU,CACxB,GAAG,EAAE,eAAe,EACpB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,oBAAoB,GACzB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAG/C"}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { MaestroMcpClient } from "../mcp/client.js";
|
|
3
|
+
import { onShutdown } from "../platform/lifecycle.js";
|
|
4
|
+
import { logger } from "../platform/logger.js";
|
|
5
|
+
/**
|
|
6
|
+
* MCP client cache — process-wide pool of started `MaestroMcpClient`s keyed on
|
|
7
|
+
* (userId, session, groupId, agentKind, serverName, specHash).
|
|
8
|
+
*
|
|
9
|
+
* Motivation: every `maestroProvider` invocation previously called
|
|
10
|
+
* `startMcpPool` which spawned every configured MCP server fresh. Most servers
|
|
11
|
+
* are stdio bun subprocesses (send-file, ocr, wiki, session-comm, etc.) and
|
|
12
|
+
* each one costs ~80–200ms to spawn + handshake. With 8–10 servers per turn
|
|
13
|
+
* that's ~1s of pure spawn latency before the model even starts thinking.
|
|
14
|
+
*
|
|
15
|
+
* Phase 1 caching reuses the same `MaestroMcpClient` across turns within the
|
|
16
|
+
* same session — the MCP `Client` connection stays open, only the
|
|
17
|
+
* `registerMcpTools` wiring is rebuilt per turn (cheap, just a Map fill).
|
|
18
|
+
*
|
|
19
|
+
* Lifecycle invariants:
|
|
20
|
+
* - One physical `MaestroMcpClient.start()` per cache key.
|
|
21
|
+
* - Per-turn callers acquire via `getOrStartClient` (refcount++) and release
|
|
22
|
+
* via `releaseClient` (refcount--). Eviction never closes a client whose
|
|
23
|
+
* refcount > 0 — an abort that closes the pool only drops the lease.
|
|
24
|
+
* - Idle TTL (`MAESTRO_MCP_POOL_IDLE_TTL_MS`) and LRU cap
|
|
25
|
+
* (`MAESTRO_MCP_POOL_MAX`) evict clients whose refcount is 0.
|
|
26
|
+
* - One process-wide `beforeExit`/`SIGINT`/`SIGTERM` handler closes every
|
|
27
|
+
* cached client on graceful shutdown so MCP subprocesses don't outlive us.
|
|
28
|
+
*
|
|
29
|
+
* Negative caching is off in Phase 1 — a failed `start()` is not cached, so
|
|
30
|
+
* the next turn retries fresh. Means a server that's transiently down doesn't
|
|
31
|
+
* stay marked-bad past one turn, at the cost of paying its spawn-fail latency
|
|
32
|
+
* again next time.
|
|
33
|
+
*/
|
|
34
|
+
/** Idle TTL — clients with refcount 0 evicted after this many ms. */
|
|
35
|
+
export const MAESTRO_MCP_POOL_IDLE_TTL_MS = Number.parseInt(process.env.MAESTRO_MCP_POOL_IDLE_TTL_MS ?? "300000", 10);
|
|
36
|
+
/** Hard cap on cached clients. LRU eviction past this. */
|
|
37
|
+
export const MAESTRO_MCP_POOL_MAX = Number.parseInt(process.env.MAESTRO_MCP_POOL_MAX ?? "16", 10);
|
|
38
|
+
const cache = new Map();
|
|
39
|
+
let sweeperHandle = null;
|
|
40
|
+
let shutdownRegistered = false;
|
|
41
|
+
let clientFactory = (name, spec) => new MaestroMcpClient(name, spec);
|
|
42
|
+
/**
|
|
43
|
+
* Build a stable hash of a server spec. We hash command + args + transport
|
|
44
|
+
* url + sorted env keys (values excluded — they often contain per-turn
|
|
45
|
+
* variables like depth, group ids; would defeat caching).
|
|
46
|
+
*
|
|
47
|
+
* Two callers with the same hash get the same client back — caller is
|
|
48
|
+
* responsible for ensuring that's semantically correct (i.e. they pass the
|
|
49
|
+
* same `CacheKeyContext` if their spec encodes the context).
|
|
50
|
+
*/
|
|
51
|
+
export function hashSpec(spec) {
|
|
52
|
+
const normalized = {
|
|
53
|
+
command: spec.command ?? null,
|
|
54
|
+
args: spec.args ?? [],
|
|
55
|
+
type: spec.type ?? null,
|
|
56
|
+
url: spec.url ?? null,
|
|
57
|
+
// env values vary per turn (e.g. session-comm --depth). Hash only the keys
|
|
58
|
+
// so an env-mutating caller still gets a cache hit on the same shape.
|
|
59
|
+
envKeys: spec.env ? Object.keys(spec.env).sort() : [],
|
|
60
|
+
};
|
|
61
|
+
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 16);
|
|
62
|
+
}
|
|
63
|
+
function makeCacheKey(ctx, serverName, specHash) {
|
|
64
|
+
return [
|
|
65
|
+
ctx.userId ?? "_",
|
|
66
|
+
ctx.session ?? "_",
|
|
67
|
+
ctx.groupId ?? "_",
|
|
68
|
+
ctx.agentKind ?? "maestro",
|
|
69
|
+
serverName,
|
|
70
|
+
specHash,
|
|
71
|
+
].join("::");
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Acquire a started `MaestroMcpClient` for (ctx, serverName, spec). If a fresh
|
|
75
|
+
* spawn is needed it happens inside this call; the returned client is already
|
|
76
|
+
* past `start()`.
|
|
77
|
+
*
|
|
78
|
+
* Refcount semantics: every successful call increments refcount by 1 and the
|
|
79
|
+
* caller MUST pair it with `releaseClient` (typically in a finally block, or
|
|
80
|
+
* inside the pool object's close()).
|
|
81
|
+
*/
|
|
82
|
+
export async function getOrStartClient(ctx, serverName, spec) {
|
|
83
|
+
ensureSweeper();
|
|
84
|
+
ensureShutdownHook();
|
|
85
|
+
const specHash = hashSpec(spec);
|
|
86
|
+
const key = makeCacheKey(ctx, serverName, specHash);
|
|
87
|
+
const existing = cache.get(key);
|
|
88
|
+
if (existing) {
|
|
89
|
+
existing.refcount++;
|
|
90
|
+
existing.lastUsed = Date.now();
|
|
91
|
+
return existing.client;
|
|
92
|
+
}
|
|
93
|
+
// Fresh spawn. We intentionally don't pre-register the entry — if start()
|
|
94
|
+
// throws we don't want a dead entry sitting in cache (negative caching off).
|
|
95
|
+
const client = clientFactory(serverName, spec);
|
|
96
|
+
await client.start();
|
|
97
|
+
// Race check: a concurrent acquire of the same key could have raced ahead
|
|
98
|
+
// while we were in `await client.start()`. If so, drop ours and use theirs.
|
|
99
|
+
const racer = cache.get(key);
|
|
100
|
+
if (racer) {
|
|
101
|
+
await client.close().catch(() => { });
|
|
102
|
+
racer.refcount++;
|
|
103
|
+
racer.lastUsed = Date.now();
|
|
104
|
+
return racer.client;
|
|
105
|
+
}
|
|
106
|
+
const entry = {
|
|
107
|
+
key,
|
|
108
|
+
client,
|
|
109
|
+
refcount: 1,
|
|
110
|
+
lastUsed: Date.now(),
|
|
111
|
+
serverName,
|
|
112
|
+
};
|
|
113
|
+
cache.set(key, entry);
|
|
114
|
+
// Enforce hard cap by evicting LRU idle entries.
|
|
115
|
+
evictOverCap();
|
|
116
|
+
return client;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Decrement refcount on a previously-acquired client. Does not close anything;
|
|
120
|
+
* the sweeper / cap-evictor closes idle clients later.
|
|
121
|
+
*
|
|
122
|
+
* `clientRef` should be the exact instance returned by `getOrStartClient`.
|
|
123
|
+
* Releasing a client we don't track is a no-op + warn (defensive against
|
|
124
|
+
* double-release).
|
|
125
|
+
*/
|
|
126
|
+
export function releaseClient(clientRef) {
|
|
127
|
+
// Linear scan — cache is small (≤ MAESTRO_MCP_POOL_MAX). Avoids a second
|
|
128
|
+
// index keyed by client identity.
|
|
129
|
+
for (const entry of cache.values()) {
|
|
130
|
+
if (entry.client === clientRef) {
|
|
131
|
+
if (entry.refcount > 0)
|
|
132
|
+
entry.refcount--;
|
|
133
|
+
entry.lastUsed = Date.now();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
logger.warn({ server: clientRef.name }, "maestro mcp pool-cache: release of unknown client (double-release?)");
|
|
138
|
+
}
|
|
139
|
+
/** Force-evict and close a single entry. Used by the sweeper / cap evictor /
|
|
140
|
+
* shutdown hook. Does nothing if the client is in use (refcount > 0). */
|
|
141
|
+
async function tryEvict(entry) {
|
|
142
|
+
if (entry.refcount > 0)
|
|
143
|
+
return false;
|
|
144
|
+
cache.delete(entry.key);
|
|
145
|
+
try {
|
|
146
|
+
await entry.client.close();
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
logger.warn({ err, server: entry.serverName }, "maestro mcp pool-cache: close on evict failed");
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
function evictOverCap() {
|
|
154
|
+
if (cache.size <= MAESTRO_MCP_POOL_MAX)
|
|
155
|
+
return;
|
|
156
|
+
const idle = [...cache.values()]
|
|
157
|
+
.filter((e) => e.refcount === 0)
|
|
158
|
+
.sort((a, b) => a.lastUsed - b.lastUsed);
|
|
159
|
+
for (const e of idle) {
|
|
160
|
+
if (cache.size <= MAESTRO_MCP_POOL_MAX)
|
|
161
|
+
return;
|
|
162
|
+
// Fire-and-forget close — the entry is already detached from cache.
|
|
163
|
+
void tryEvict(e);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Run one idle-TTL sweep. Exported for tests. */
|
|
167
|
+
export async function sweepIdle(now = Date.now()) {
|
|
168
|
+
let evicted = 0;
|
|
169
|
+
const stale = [...cache.values()].filter((e) => e.refcount === 0 && now - e.lastUsed >= MAESTRO_MCP_POOL_IDLE_TTL_MS);
|
|
170
|
+
for (const e of stale) {
|
|
171
|
+
if (await tryEvict(e))
|
|
172
|
+
evicted++;
|
|
173
|
+
}
|
|
174
|
+
return evicted;
|
|
175
|
+
}
|
|
176
|
+
function ensureSweeper() {
|
|
177
|
+
if (sweeperHandle)
|
|
178
|
+
return;
|
|
179
|
+
// Half the TTL keeps the worst-case eviction lag at ~1.5× TTL.
|
|
180
|
+
const interval = Math.max(30_000, Math.floor(MAESTRO_MCP_POOL_IDLE_TTL_MS / 2));
|
|
181
|
+
sweeperHandle = setInterval(() => {
|
|
182
|
+
sweepIdle().catch((err) => {
|
|
183
|
+
logger.warn({ err }, "maestro mcp pool-cache: sweep failed");
|
|
184
|
+
});
|
|
185
|
+
}, interval);
|
|
186
|
+
// unref so the interval doesn't keep the process alive at shutdown.
|
|
187
|
+
if (typeof sweeperHandle.unref === "function")
|
|
188
|
+
sweeperHandle.unref();
|
|
189
|
+
}
|
|
190
|
+
/** Stop the sweeper. Used by tests to reset state. */
|
|
191
|
+
export function stopSweeper() {
|
|
192
|
+
if (sweeperHandle) {
|
|
193
|
+
clearInterval(sweeperHandle);
|
|
194
|
+
sweeperHandle = null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Close every cached client. Awaits up to `timeoutMs` total (3s default).
|
|
199
|
+
* Safe to call multiple times — entries are removed as they close.
|
|
200
|
+
*/
|
|
201
|
+
export async function closeAll(timeoutMs = 3000) {
|
|
202
|
+
const entries = [...cache.values()];
|
|
203
|
+
cache.clear();
|
|
204
|
+
const closes = entries.map(async (e) => {
|
|
205
|
+
try {
|
|
206
|
+
await e.client.close();
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
logger.warn({ err, server: e.serverName }, "maestro mcp pool-cache: close during shutdown failed");
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
await Promise.race([
|
|
213
|
+
Promise.allSettled(closes),
|
|
214
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
|
215
|
+
]);
|
|
216
|
+
}
|
|
217
|
+
function ensureShutdownHook() {
|
|
218
|
+
if (shutdownRegistered)
|
|
219
|
+
return;
|
|
220
|
+
shutdownRegistered = true;
|
|
221
|
+
// High priority: graceful close of MCP clients should finish BEFORE
|
|
222
|
+
// other shutdown handlers start killing external processes (PII server,
|
|
223
|
+
// playwright children, codex trees) — otherwise an in-flight MCP tool
|
|
224
|
+
// call could lose its subprocess mid-handshake.
|
|
225
|
+
onShutdown("maestro-mcp-pool", 100, () => closeAll());
|
|
226
|
+
}
|
|
227
|
+
/** Test-only: drop all entries without closing. Used to reset state between
|
|
228
|
+
* test runs that share the module singleton. */
|
|
229
|
+
export function __resetForTests() {
|
|
230
|
+
cache.clear();
|
|
231
|
+
stopSweeper();
|
|
232
|
+
shutdownRegistered = false;
|
|
233
|
+
clientFactory = (name, spec) => new MaestroMcpClient(name, spec);
|
|
234
|
+
}
|
|
235
|
+
/** Test-only: swap the client factory. Used to inject a fake client backed by
|
|
236
|
+
* `InMemoryTransport` so tests don't spawn real MCP subprocesses. */
|
|
237
|
+
export function __setClientFactoryForTests(factory) {
|
|
238
|
+
clientFactory = factory;
|
|
239
|
+
}
|
|
240
|
+
/** Test-only: read cache size. */
|
|
241
|
+
export function __cacheSize() {
|
|
242
|
+
return cache.size;
|
|
243
|
+
}
|
|
244
|
+
/** Test-only: read entry by key parts. */
|
|
245
|
+
export function __getEntry(ctx, serverName, spec) {
|
|
246
|
+
const e = cache.get(makeCacheKey(ctx, serverName, hashSpec(spec)));
|
|
247
|
+
return e ? { refcount: e.refcount, lastUsed: e.lastUsed } : null;
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=pool-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool-cache.js","sourceRoot":"","sources":["../../src/mcp/pool-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { MaestroMcpClient, MaestroMcpTool } from "../mcp/client.js";
|
|
2
|
+
import { type CacheKeyContext } from "../mcp/pool-cache.js";
|
|
3
|
+
import type { ToolRegistry } from "../tools/registry.js";
|
|
4
|
+
/**
|
|
5
|
+
* MCP pool — per-turn lease view over the process-wide cache in `pool-cache.ts`.
|
|
6
|
+
*
|
|
7
|
+
* Each `maestroProvider` call builds one pool, which acquires a refcounted lease
|
|
8
|
+
* on every configured MCP client from the cache (spawning fresh on cache miss).
|
|
9
|
+
* When the turn ends, `pool.close()` releases the leases — the actual MCP
|
|
10
|
+
* subprocess stays alive, ready for the next turn within the same session.
|
|
11
|
+
* Eviction (idle TTL or LRU cap) happens in `pool-cache.ts`.
|
|
12
|
+
*
|
|
13
|
+
* What's the same as the pre-cache pool:
|
|
14
|
+
* - Failure-isolation via `Promise.allSettled` (one bad server doesn't take
|
|
15
|
+
* out the rest).
|
|
16
|
+
* - `mcp__<server>__<tool>` naming consistent with Claude SDK.
|
|
17
|
+
* - builtin/MCP tool-name collisions: builtin wins + warn (handled in
|
|
18
|
+
* `registerMcpTools` here).
|
|
19
|
+
* - `tools/call` abortSignal still plumbed through so an aborted turn
|
|
20
|
+
* cancels in-flight RPCs without killing the (still-cached) client.
|
|
21
|
+
*
|
|
22
|
+
* What's different with caching:
|
|
23
|
+
* - `pool.close()` now releases leases, not closes clients.
|
|
24
|
+
* - `startMcpPool` takes a `CacheKeyContext` so two users (or two sessions
|
|
25
|
+
* within the same user) get separate cached clients even when the spec
|
|
26
|
+
* hash matches.
|
|
27
|
+
*/
|
|
28
|
+
export interface MaestroMcpPool {
|
|
29
|
+
/** Live clients leased for this turn, indexed by server name. */
|
|
30
|
+
readonly clients: MaestroMcpClient[];
|
|
31
|
+
/** All tools that successfully listed across every client. */
|
|
32
|
+
readonly tools: MaestroMcpTool[];
|
|
33
|
+
/** Release the leases. Safe to call multiple times. */
|
|
34
|
+
close(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Acquire a lease on every MCP server in `servers` from the cache and collect
|
|
38
|
+
* their tool schemas. On cache miss the server is spawned fresh; on hit the
|
|
39
|
+
* pre-warmed client is reused (no spawn cost, no handshake cost).
|
|
40
|
+
*
|
|
41
|
+
* Failure isolation: one server failing to start does not abort the others,
|
|
42
|
+
* we log and continue. Partial MCP availability is preferable to losing the
|
|
43
|
+
* whole toolset for a turn — same stance as the pre-cache implementation and
|
|
44
|
+
* the Playwright exit-propagation path.
|
|
45
|
+
*
|
|
46
|
+
* `ctx` scopes the cache key. Pass userId/session/groupId from `AgentQueryOptions`.
|
|
47
|
+
* Same spec across two contexts ⇒ two separate cached clients (correct: e.g.
|
|
48
|
+
* playwright stdio with `--user-data-dir` differs per user-session anyway, but
|
|
49
|
+
* scoping by context keeps the invariant even when specs happen to match).
|
|
50
|
+
*/
|
|
51
|
+
export declare function startMcpPool(servers: Record<string, unknown>, ctx?: CacheKeyContext): Promise<MaestroMcpPool>;
|
|
52
|
+
/**
|
|
53
|
+
* Register every MCP tool in the pool into the given Maestro ToolRegistry.
|
|
54
|
+
*
|
|
55
|
+
* Name collisions (e.g. a builtin already owns the same prefixed name) are
|
|
56
|
+
* logged and skipped rather than thrown — builtins win because they're more
|
|
57
|
+
* stable than MCP servers that come and go per turn.
|
|
58
|
+
*
|
|
59
|
+
* Optional `abortSignal` is forwarded into every MCP `tools/call` so a
|
|
60
|
+
* Maestro-level abort cancels in-flight JSON-RPC requests instead of leaving
|
|
61
|
+
* them blocked on a dead/slow server. The cached client itself stays alive —
|
|
62
|
+
* only the in-flight RPC is cancelled, so the next turn can reuse it.
|
|
63
|
+
*/
|
|
64
|
+
export declare function registerMcpTools(registry: ToolRegistry, pool: MaestroMcpPool, abortSignal?: AbortSignal): void;
|
|
65
|
+
//# sourceMappingURL=pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../src/mcp/pool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAwB,cAAc,EAAE,MAAM,cAAc,CAAC;AAC3F,OAAO,EAAE,KAAK,eAAe,EAAmC,MAAM,kBAAkB,CAAC;AACzF,OAAO,KAAK,EAAe,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAGlE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,gBAAgB,EAAE,CAAC;IACrC,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,CAAC;IACjC,uDAAuD;IACvD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,GAAG,GAAE,eAAoB,GACxB,OAAO,CAAC,cAAc,CAAC,CAuCzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,YAAY,EACtB,IAAI,EAAE,cAAc,EACpB,WAAW,CAAC,EAAE,WAAW,GACxB,IAAI,CAwBN"}
|