@kenkaiiii/ggcoder 5.34.3 → 5.35.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/dist/app-sidecar.js +11 -0
- package/dist/app-sidecar.js.map +1 -1
- package/dist/core/lsp/manager.d.ts +44 -9
- package/dist/core/lsp/manager.d.ts.map +1 -1
- package/dist/core/lsp/manager.js +64 -69
- package/dist/core/lsp/manager.js.map +1 -1
- package/dist/core/lsp/pool.d.ts +113 -0
- package/dist/core/lsp/pool.d.ts.map +1 -0
- package/dist/core/lsp/pool.js +255 -0
- package/dist/core/lsp/pool.js.map +1 -0
- package/dist/core/lsp/pool.test.d.ts +2 -0
- package/dist/core/lsp/pool.test.d.ts.map +1 -0
- package/dist/core/lsp/pool.test.js +286 -0
- package/dist/core/lsp/pool.test.js.map +1 -0
- package/dist/core/lsp/servers.d.ts.map +1 -1
- package/dist/core/lsp/servers.js +44 -3
- package/dist/core/lsp/servers.js.map +1 -1
- package/dist/core/lsp/servers.test.d.ts +2 -0
- package/dist/core/lsp/servers.test.d.ts.map +1 -0
- package/dist/core/lsp/servers.test.js +67 -0
- package/dist/core/lsp/servers.test.js.map +1 -0
- package/dist/core/lsp/windows.test.js +2 -14
- package/dist/core/lsp/windows.test.js.map +1 -1
- package/dist/core/mcp/client.d.ts +40 -0
- package/dist/core/mcp/client.d.ts.map +1 -1
- package/dist/core/mcp/client.js +119 -1
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/mcp/index.d.ts +2 -0
- package/dist/core/mcp/index.d.ts.map +1 -1
- package/dist/core/mcp/index.js +1 -0
- package/dist/core/mcp/index.js.map +1 -1
- package/dist/core/mcp/session-elicitation-wiring.test.d.ts +2 -0
- package/dist/core/mcp/session-elicitation-wiring.test.d.ts.map +1 -0
- package/dist/core/mcp/session-elicitation-wiring.test.js +138 -0
- package/dist/core/mcp/session-elicitation-wiring.test.js.map +1 -0
- package/dist/core/mcp/shared-pool.d.ts +158 -0
- package/dist/core/mcp/shared-pool.d.ts.map +1 -0
- package/dist/core/mcp/shared-pool.js +235 -0
- package/dist/core/mcp/shared-pool.js.map +1 -0
- package/dist/core/mcp/shared-pool.test.d.ts +2 -0
- package/dist/core/mcp/shared-pool.test.d.ts.map +1 -0
- package/dist/core/mcp/shared-pool.test.js +425 -0
- package/dist/core/mcp/shared-pool.test.js.map +1 -0
- package/dist/core/mcp/types.d.ts +12 -0
- package/dist/core/mcp/types.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { SharedMcpPool } from "./shared-pool.js";
|
|
6
|
+
/**
|
|
7
|
+
* Guards the wiring that makes one MCP process serve a whole window.
|
|
8
|
+
*
|
|
9
|
+
* A daemon window runs THREE sessions — the coding agent, Ken chat, and Ken
|
|
10
|
+
* autopilot — and every one of them connects to the same MCP servers. They only
|
|
11
|
+
* collapse onto a single child process if they agree on one thing: whether they
|
|
12
|
+
* can prompt the user.
|
|
13
|
+
*
|
|
14
|
+
* That is not a stylistic detail. A connection declares its elicitation
|
|
15
|
+
* capability once, at initialize, and servers use that declaration to decide
|
|
16
|
+
* whether to ask at all — so the pool cannot hand a prompting session and a
|
|
17
|
+
* headless one the same connection without lying to one of them. It keys them
|
|
18
|
+
* apart instead (see shared-pool.ts `keyFor`).
|
|
19
|
+
*
|
|
20
|
+
* The regression this file exists to catch: Ken's two factories originally
|
|
21
|
+
* omitted `onMcpElicit`, which made them headless, which split the pool, which
|
|
22
|
+
* produced a SECOND kencode-search process per daemon — measured, before the
|
|
23
|
+
* fix, as 2 processes for 6 sessions instead of 1. Nothing else failed, which is
|
|
24
|
+
* exactly why it needs a test.
|
|
25
|
+
*/
|
|
26
|
+
const APP_SIDECAR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../app-sidecar.ts");
|
|
27
|
+
/**
|
|
28
|
+
* Extract the object literal passed to each `new AgentSession({ ... })`.
|
|
29
|
+
*
|
|
30
|
+
* Brace-balanced rather than regex-terminated: these literals contain nested
|
|
31
|
+
* callbacks with their own braces, so a non-greedy match would stop at the
|
|
32
|
+
* first inner `}` and silently "pass" by inspecting a fragment.
|
|
33
|
+
*/
|
|
34
|
+
function agentSessionOptionBlocks(source) {
|
|
35
|
+
const blocks = [];
|
|
36
|
+
const marker = "new AgentSession({";
|
|
37
|
+
let from = 0;
|
|
38
|
+
for (;;) {
|
|
39
|
+
const start = source.indexOf(marker, from);
|
|
40
|
+
if (start === -1)
|
|
41
|
+
return blocks;
|
|
42
|
+
let depth = 0;
|
|
43
|
+
let end = start + marker.length - 1;
|
|
44
|
+
for (; end < source.length; end++) {
|
|
45
|
+
const ch = source[end];
|
|
46
|
+
if (ch === "{")
|
|
47
|
+
depth++;
|
|
48
|
+
else if (ch === "}") {
|
|
49
|
+
depth--;
|
|
50
|
+
if (depth === 0)
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
blocks.push(source.slice(start, end + 1));
|
|
55
|
+
from = end + 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
describe("app-sidecar wires every window session for elicitation", () => {
|
|
59
|
+
it("passes an elicit handler to all three session factories", async () => {
|
|
60
|
+
const source = await fs.readFile(APP_SIDECAR, "utf8");
|
|
61
|
+
const blocks = agentSessionOptionBlocks(source);
|
|
62
|
+
// Coding agent + Ken chat + Ken autopilot. If this count changes, a new
|
|
63
|
+
// session kind was added and it needs the same wiring decision made
|
|
64
|
+
// deliberately rather than inherited by accident.
|
|
65
|
+
expect(blocks).toHaveLength(3);
|
|
66
|
+
for (const block of blocks) {
|
|
67
|
+
// Either wired directly, or via the shared base options object that
|
|
68
|
+
// carries `onMcpElicit` for the coding session.
|
|
69
|
+
const wired = block.includes("onMcpElicit") || block.includes("...baseSessionOptions");
|
|
70
|
+
expect(wired).toBe(true);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
/**
|
|
74
|
+
* Named factories specifically, so the assertion above cannot be satisfied by
|
|
75
|
+
* three blocks that happen to be the wrong three.
|
|
76
|
+
*/
|
|
77
|
+
it("wires Ken chat and Ken autopilot specifically, not just the coding agent", async () => {
|
|
78
|
+
const source = await fs.readFile(APP_SIDECAR, "utf8");
|
|
79
|
+
for (const factory of ["ensureKenSession", "ensureKenAutoSession"]) {
|
|
80
|
+
const start = source.indexOf(`async function ${factory}(`);
|
|
81
|
+
expect(start, `${factory} should exist`).toBeGreaterThan(-1);
|
|
82
|
+
const [options] = agentSessionOptionBlocks(source.slice(start));
|
|
83
|
+
expect(options, `${factory} should construct an AgentSession`).toBeDefined();
|
|
84
|
+
expect(options).toContain("onMcpElicit");
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
/** `onMcpElicit` must still be the option AgentSession forwards to MCP. */
|
|
88
|
+
it("forwards that option into the MCP client as onElicit", async () => {
|
|
89
|
+
const agentSession = await fs.readFile(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../agent-session.ts"), "utf8");
|
|
90
|
+
expect(agentSession).toContain("onElicit: this.opts.onMcpElicit");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe("pool keying for a window's three sessions", () => {
|
|
94
|
+
const config = { name: "kencode-search", command: "npx" };
|
|
95
|
+
function connector(spawns) {
|
|
96
|
+
spawns.count += 1;
|
|
97
|
+
return {
|
|
98
|
+
connect: async (target) => ({
|
|
99
|
+
name: target.name,
|
|
100
|
+
ok: true,
|
|
101
|
+
toolCount: 0,
|
|
102
|
+
tools: [],
|
|
103
|
+
}),
|
|
104
|
+
dispose: async () => { },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const handler = async () => ({ action: "cancel" });
|
|
108
|
+
/** The shipped arrangement: coding + Ken chat + Ken autopilot, all windowed. */
|
|
109
|
+
it("gives all three a single shared connection when each can prompt", async () => {
|
|
110
|
+
const pool = new SharedMcpPool();
|
|
111
|
+
const spawns = { count: 0 };
|
|
112
|
+
const coding = await pool.acquire(config, () => connector(spawns), { onElicit: handler });
|
|
113
|
+
const kenChat = await pool.acquire(config, () => connector(spawns), { onElicit: handler });
|
|
114
|
+
const kenAuto = await pool.acquire(config, () => connector(spawns), { onElicit: handler });
|
|
115
|
+
expect(spawns.count).toBe(1);
|
|
116
|
+
expect(pool.size).toBe(1);
|
|
117
|
+
expect(pool.refCount(config, { onElicit: handler })).toBe(3);
|
|
118
|
+
await coding.release();
|
|
119
|
+
await kenChat.release();
|
|
120
|
+
await kenAuto.release();
|
|
121
|
+
expect(pool.size).toBe(0);
|
|
122
|
+
});
|
|
123
|
+
/**
|
|
124
|
+
* The regression, reproduced: drop the handler from Ken's sessions and the
|
|
125
|
+
* pool splits — one connection for the coding agent, a second for the two
|
|
126
|
+
* headless Ken sessions. Two processes where there should be one.
|
|
127
|
+
*/
|
|
128
|
+
it("splits into a second connection when Ken's sessions cannot prompt", async () => {
|
|
129
|
+
const pool = new SharedMcpPool();
|
|
130
|
+
const spawns = { count: 0 };
|
|
131
|
+
await pool.acquire(config, () => connector(spawns), { onElicit: handler });
|
|
132
|
+
await pool.acquire(config, () => connector(spawns), {});
|
|
133
|
+
await pool.acquire(config, () => connector(spawns), {});
|
|
134
|
+
expect(spawns.count).toBe(2);
|
|
135
|
+
expect(pool.size).toBe(2);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
//# sourceMappingURL=session-elicitation-wiring.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-elicitation-wiring.test.js","sourceRoot":"","sources":["../../../src/core/mcp/session-elicitation-wiring.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { McpCatalogCache } from "./catalog-cache.js";
|
|
2
|
+
import type { MCPConnectResult, MCPElicitHandler } from "./client.js";
|
|
3
|
+
import type { MCPServerConfig } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Process-wide pool of MCP connections that are safe for every session to
|
|
6
|
+
* share, so one stdio child serves the whole daemon instead of one per session.
|
|
7
|
+
*
|
|
8
|
+
* The daemon hosts many `AgentSession`s at once — one per window, plus Ken chat
|
|
9
|
+
* and Ken autopilot within each window — and each used to spawn its own child
|
|
10
|
+
* for every configured stdio server. For a stateless proxy such as
|
|
11
|
+
* `kencode-search` that is N identical processes doing identical work: measured
|
|
12
|
+
* at 7 live children, ~43 MB each, purely duplicated.
|
|
13
|
+
*
|
|
14
|
+
* Sharing is the DEFAULT for stdio servers. A stdio connection has nothing
|
|
15
|
+
* session-specific in it: every one is spawned with `cwd: os.homedir()`, so a
|
|
16
|
+
* server cannot observe which project the caller is in even in principle, and
|
|
17
|
+
* the pool key hashes command/args/env, so two configs differing in any way
|
|
18
|
+
* that changes behaviour already get their own process.
|
|
19
|
+
*
|
|
20
|
+
* The two concerns that sharing has to answer are handled rather than avoided:
|
|
21
|
+
*
|
|
22
|
+
* - **Per-caller state.** Multiplexing many sessions over one connection would
|
|
23
|
+
* let a server that remembers "the current project" leak one session's
|
|
24
|
+
* context into another's answers. That is invisible from a config, so it
|
|
25
|
+
* stays a declaration: `shared: false` opts such a server out and it keeps a
|
|
26
|
+
* private child per session.
|
|
27
|
+
* - **User prompting.** "Ask the user" has to name a window, so elicitation is
|
|
28
|
+
* routed to the session with a tool call in flight (see `dispatchElicit`),
|
|
29
|
+
* and cancelled rather than guessed when that is ambiguous. Because a
|
|
30
|
+
* connection declares its elicitation capability once at initialize, callers
|
|
31
|
+
* that can prompt and callers that cannot are keyed apart.
|
|
32
|
+
*
|
|
33
|
+
* The pool deliberately knows nothing about `MCPClientManager` — the connection
|
|
34
|
+
* is supplied as a `SharedConnector`. That keeps the dependency pointing one
|
|
35
|
+
* way (client → pool), and lets tests exercise refcounting against a fake.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Is this server shared across sessions in this process?
|
|
39
|
+
*
|
|
40
|
+
* Sharing is the DEFAULT for stdio servers, because a stdio MCP connection has
|
|
41
|
+
* nothing session-specific in it: every one is spawned with `cwd:
|
|
42
|
+
* os.homedir()` (see client.ts), so a server cannot observe which project the
|
|
43
|
+
* caller is in even in principle, and the pool key hashes command/args/env, so
|
|
44
|
+
* two configs that differ in any way that changes behaviour already get their
|
|
45
|
+
* own process. What remained — elicitation, the one genuinely per-window
|
|
46
|
+
* concern — is routed back to the calling session rather than dropped.
|
|
47
|
+
*
|
|
48
|
+
* `shared: false` opts a server out. That exists for a server that keeps
|
|
49
|
+
* per-CALLER state across requests (a cursor, a selected workspace, an open
|
|
50
|
+
* handle), where multiplexing two sessions over one connection would let one
|
|
51
|
+
* session's state change the other's answers. That property is invisible from
|
|
52
|
+
* a config, so it stays a declaration rather than a guess.
|
|
53
|
+
*
|
|
54
|
+
* HTTP servers are never shared. Sharing exists to collapse duplicate child
|
|
55
|
+
* PROCESSES and an HTTP server has none, so it would save nothing while adding
|
|
56
|
+
* real risk: OAuth tokens and `Mcp-Session-Id` are per-connection, and pooling
|
|
57
|
+
* them would cross one session's authenticated identity with another's.
|
|
58
|
+
*/
|
|
59
|
+
export declare function isShareableServer(config: MCPServerConfig): boolean;
|
|
60
|
+
/** One pooled connection, owned by the pool and torn down at zero references. */
|
|
61
|
+
export interface SharedConnector {
|
|
62
|
+
connect(config: MCPServerConfig): Promise<MCPConnectResult>;
|
|
63
|
+
dispose(): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
export interface SharedAcquireOptions {
|
|
66
|
+
catalogCache?: McpCatalogCache;
|
|
67
|
+
modernProtocol?: boolean;
|
|
68
|
+
/** Invoked by the connection when its server exits on its own. */
|
|
69
|
+
onClosed?: () => void;
|
|
70
|
+
/**
|
|
71
|
+
* The acquiring session's elicitation handler. Invoked only while that
|
|
72
|
+
* session has a tool call in flight on this connection (see `dispatchElicit`).
|
|
73
|
+
*/
|
|
74
|
+
onElicit?: MCPElicitHandler;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Builds the underlying connection the first time a config is pooled. The pool
|
|
78
|
+
* supplies its own `onElicit`: a dispatcher that routes each request to the
|
|
79
|
+
* session that asked for it, instead of any one session's handler.
|
|
80
|
+
*/
|
|
81
|
+
export type SharedConnectorFactory = (opts: SharedAcquireOptions) => SharedConnector;
|
|
82
|
+
/** A session's claim on a shared connection. `release` is idempotent. */
|
|
83
|
+
export interface SharedServerHandle {
|
|
84
|
+
result: MCPConnectResult;
|
|
85
|
+
release: () => Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Mark a tool call from this session as in flight, returning the function
|
|
88
|
+
* that ends it. Elicitation arriving during the call is routed to this
|
|
89
|
+
* session's window.
|
|
90
|
+
*/
|
|
91
|
+
beginCall: () => () => void;
|
|
92
|
+
}
|
|
93
|
+
export declare class SharedMcpPool {
|
|
94
|
+
private entries;
|
|
95
|
+
/**
|
|
96
|
+
* Key on everything that changes what the connection IS, not merely what it
|
|
97
|
+
* exposes: `hashServerConfig` covers command/args/env/url/transport, the name
|
|
98
|
+
* keeps two differently-named aliases apart, and the protocol flag is
|
|
99
|
+
* included because it changes the handshake — a session that opted into the
|
|
100
|
+
* modern revision must not be handed a legacy-negotiated connection.
|
|
101
|
+
*
|
|
102
|
+
* Whether the caller can prompt is part of the key for the same reason. A
|
|
103
|
+
* connection declares its elicitation capability once, at initialize, and
|
|
104
|
+
* servers use that declaration to decide whether to ask at all — so a
|
|
105
|
+
* headless caller (CLI, JSON mode) must not be handed a connection that
|
|
106
|
+
* promises prompting nobody can deliver, and a windowed caller must not be
|
|
107
|
+
* handed one that forecloses it. Splitting the key keeps each connection's
|
|
108
|
+
* declaration honest; in practice a daemon's sessions are uniformly windowed
|
|
109
|
+
* and the CLI's uniformly headless, so this still means one process.
|
|
110
|
+
*/
|
|
111
|
+
private keyFor;
|
|
112
|
+
/**
|
|
113
|
+
* Get-or-create the shared connection for `config` and claim a reference.
|
|
114
|
+
*
|
|
115
|
+
* Concurrent acquirers share one connect attempt: the entry and its pending
|
|
116
|
+
* promise are registered synchronously before the first `await`, so a second
|
|
117
|
+
* caller arriving mid-connect joins the in-flight attempt instead of spawning
|
|
118
|
+
* a rival child.
|
|
119
|
+
*/
|
|
120
|
+
acquire(config: MCPServerConfig, createConnector: SharedConnectorFactory, opts?: SharedAcquireOptions): Promise<SharedServerHandle>;
|
|
121
|
+
/**
|
|
122
|
+
* Drop a connection whose server died, without touching its callers' claims.
|
|
123
|
+
*
|
|
124
|
+
* The entry is unregistered but NOT disposed: its transport is already gone,
|
|
125
|
+
* and the sessions still holding handles will release them normally on their
|
|
126
|
+
* own dispose. Guarded on identity so a death notice arriving after the entry
|
|
127
|
+
* was already replaced cannot evict its successor.
|
|
128
|
+
*/
|
|
129
|
+
private evictDead;
|
|
130
|
+
/**
|
|
131
|
+
* Route an elicitation to the session that provoked it.
|
|
132
|
+
*
|
|
133
|
+
* A shared connection is held by many windows, so "ask the user" has to name
|
|
134
|
+
* one. Elicitation only happens while a server is servicing a tool call, so
|
|
135
|
+
* the session with a call in flight IS the one whose window should show the
|
|
136
|
+
* form. When that is ambiguous — no call in flight, or two windows calling
|
|
137
|
+
* the same server at once — the request is cancelled rather than guessed:
|
|
138
|
+
* showing one project's consent form in another project's window would be a
|
|
139
|
+
* worse failure than a declined prompt, and `cancel` is exactly what a server
|
|
140
|
+
* already handles for a user who dismissed the dialog.
|
|
141
|
+
*/
|
|
142
|
+
private dispatchElicit;
|
|
143
|
+
/**
|
|
144
|
+
* Drop one session's claim. At zero the entry is unregistered SYNCHRONOUSLY
|
|
145
|
+
* before the disposal await, so an `acquire` landing mid-teardown builds a
|
|
146
|
+
* fresh connection instead of receiving a client that is already closing.
|
|
147
|
+
*/
|
|
148
|
+
private releaseKey;
|
|
149
|
+
/** Live shared connections. Test/diagnostic use. */
|
|
150
|
+
get size(): number;
|
|
151
|
+
/** Reference count for one config, or 0 when nothing is pooled for it. */
|
|
152
|
+
refCount(config: MCPServerConfig, opts?: SharedAcquireOptions): number;
|
|
153
|
+
/** Tear every shared connection down regardless of refs (process shutdown). */
|
|
154
|
+
disposeAll(): Promise<void>;
|
|
155
|
+
}
|
|
156
|
+
/** The daemon-wide pool. One per process, which is exactly the sharing scope. */
|
|
157
|
+
export declare const sharedMcpPool: SharedMcpPool;
|
|
158
|
+
//# sourceMappingURL=shared-pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-pool.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/shared-pool.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAElE;AAED,iFAAiF;AACjF,MAAM,WAAW,eAAe;IAC9B,OAAO,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC5D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,YAAY,CAAC,EAAE,eAAe,CAAC;IAC/B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,gBAAgB,CAAC;CAC7B;AAED;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,IAAI,EAAE,oBAAoB,KAAK,eAAe,CAAC;AAErF,yEAAyE;AACzE,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,gBAAgB,CAAC;IACzB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B;;;;OAIG;IACH,SAAS,EAAE,MAAM,MAAM,IAAI,CAAC;CAC7B;AAgBD,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAAgC;IAE/C;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,MAAM;IAMd;;;;;;;OAOG;IACG,OAAO,CACX,MAAM,EAAE,eAAe,EACvB,eAAe,EAAE,sBAAsB,EACvC,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,kBAAkB,CAAC;IAwE9B;;;;;;;OAOG;IACH,OAAO,CAAC,SAAS;IAMjB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,cAAc;IActB;;;;OAIG;YACW,UAAU;IAUxB,oDAAoD;IACpD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,eAAe,EAAE,IAAI,GAAE,oBAAyB,GAAG,MAAM;IAI1E,+EAA+E;IACzE,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;CAKlC;AAED,iFAAiF;AACjF,eAAO,MAAM,aAAa,eAAsB,CAAC"}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { hashServerConfig } from "./catalog-cache.js";
|
|
2
|
+
import { log } from "../logger.js";
|
|
3
|
+
/**
|
|
4
|
+
* Process-wide pool of MCP connections that are safe for every session to
|
|
5
|
+
* share, so one stdio child serves the whole daemon instead of one per session.
|
|
6
|
+
*
|
|
7
|
+
* The daemon hosts many `AgentSession`s at once — one per window, plus Ken chat
|
|
8
|
+
* and Ken autopilot within each window — and each used to spawn its own child
|
|
9
|
+
* for every configured stdio server. For a stateless proxy such as
|
|
10
|
+
* `kencode-search` that is N identical processes doing identical work: measured
|
|
11
|
+
* at 7 live children, ~43 MB each, purely duplicated.
|
|
12
|
+
*
|
|
13
|
+
* Sharing is the DEFAULT for stdio servers. A stdio connection has nothing
|
|
14
|
+
* session-specific in it: every one is spawned with `cwd: os.homedir()`, so a
|
|
15
|
+
* server cannot observe which project the caller is in even in principle, and
|
|
16
|
+
* the pool key hashes command/args/env, so two configs differing in any way
|
|
17
|
+
* that changes behaviour already get their own process.
|
|
18
|
+
*
|
|
19
|
+
* The two concerns that sharing has to answer are handled rather than avoided:
|
|
20
|
+
*
|
|
21
|
+
* - **Per-caller state.** Multiplexing many sessions over one connection would
|
|
22
|
+
* let a server that remembers "the current project" leak one session's
|
|
23
|
+
* context into another's answers. That is invisible from a config, so it
|
|
24
|
+
* stays a declaration: `shared: false` opts such a server out and it keeps a
|
|
25
|
+
* private child per session.
|
|
26
|
+
* - **User prompting.** "Ask the user" has to name a window, so elicitation is
|
|
27
|
+
* routed to the session with a tool call in flight (see `dispatchElicit`),
|
|
28
|
+
* and cancelled rather than guessed when that is ambiguous. Because a
|
|
29
|
+
* connection declares its elicitation capability once at initialize, callers
|
|
30
|
+
* that can prompt and callers that cannot are keyed apart.
|
|
31
|
+
*
|
|
32
|
+
* The pool deliberately knows nothing about `MCPClientManager` — the connection
|
|
33
|
+
* is supplied as a `SharedConnector`. That keeps the dependency pointing one
|
|
34
|
+
* way (client → pool), and lets tests exercise refcounting against a fake.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Is this server shared across sessions in this process?
|
|
38
|
+
*
|
|
39
|
+
* Sharing is the DEFAULT for stdio servers, because a stdio MCP connection has
|
|
40
|
+
* nothing session-specific in it: every one is spawned with `cwd:
|
|
41
|
+
* os.homedir()` (see client.ts), so a server cannot observe which project the
|
|
42
|
+
* caller is in even in principle, and the pool key hashes command/args/env, so
|
|
43
|
+
* two configs that differ in any way that changes behaviour already get their
|
|
44
|
+
* own process. What remained — elicitation, the one genuinely per-window
|
|
45
|
+
* concern — is routed back to the calling session rather than dropped.
|
|
46
|
+
*
|
|
47
|
+
* `shared: false` opts a server out. That exists for a server that keeps
|
|
48
|
+
* per-CALLER state across requests (a cursor, a selected workspace, an open
|
|
49
|
+
* handle), where multiplexing two sessions over one connection would let one
|
|
50
|
+
* session's state change the other's answers. That property is invisible from
|
|
51
|
+
* a config, so it stays a declaration rather than a guess.
|
|
52
|
+
*
|
|
53
|
+
* HTTP servers are never shared. Sharing exists to collapse duplicate child
|
|
54
|
+
* PROCESSES and an HTTP server has none, so it would save nothing while adding
|
|
55
|
+
* real risk: OAuth tokens and `Mcp-Session-Id` are per-connection, and pooling
|
|
56
|
+
* them would cross one session's authenticated identity with another's.
|
|
57
|
+
*/
|
|
58
|
+
export function isShareableServer(config) {
|
|
59
|
+
return config.shared !== false && Boolean(config.command);
|
|
60
|
+
}
|
|
61
|
+
export class SharedMcpPool {
|
|
62
|
+
entries = new Map();
|
|
63
|
+
/**
|
|
64
|
+
* Key on everything that changes what the connection IS, not merely what it
|
|
65
|
+
* exposes: `hashServerConfig` covers command/args/env/url/transport, the name
|
|
66
|
+
* keeps two differently-named aliases apart, and the protocol flag is
|
|
67
|
+
* included because it changes the handshake — a session that opted into the
|
|
68
|
+
* modern revision must not be handed a legacy-negotiated connection.
|
|
69
|
+
*
|
|
70
|
+
* Whether the caller can prompt is part of the key for the same reason. A
|
|
71
|
+
* connection declares its elicitation capability once, at initialize, and
|
|
72
|
+
* servers use that declaration to decide whether to ask at all — so a
|
|
73
|
+
* headless caller (CLI, JSON mode) must not be handed a connection that
|
|
74
|
+
* promises prompting nobody can deliver, and a windowed caller must not be
|
|
75
|
+
* handed one that forecloses it. Splitting the key keeps each connection's
|
|
76
|
+
* declaration honest; in practice a daemon's sessions are uniformly windowed
|
|
77
|
+
* and the CLI's uniformly headless, so this still means one process.
|
|
78
|
+
*/
|
|
79
|
+
keyFor(config, opts) {
|
|
80
|
+
const era = opts.modernProtocol ? "modern" : "legacy";
|
|
81
|
+
const prompting = opts.onElicit ? "interactive" : "headless";
|
|
82
|
+
return `${config.name}\u0000${hashServerConfig(config)}\u0000${era}\u0000${prompting}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get-or-create the shared connection for `config` and claim a reference.
|
|
86
|
+
*
|
|
87
|
+
* Concurrent acquirers share one connect attempt: the entry and its pending
|
|
88
|
+
* promise are registered synchronously before the first `await`, so a second
|
|
89
|
+
* caller arriving mid-connect joins the in-flight attempt instead of spawning
|
|
90
|
+
* a rival child.
|
|
91
|
+
*/
|
|
92
|
+
async acquire(config, createConnector, opts = {}) {
|
|
93
|
+
const key = this.keyFor(config, opts);
|
|
94
|
+
const caller = { onElicit: opts.onElicit, activeCalls: 0 };
|
|
95
|
+
let entry = this.entries.get(key);
|
|
96
|
+
if (!entry) {
|
|
97
|
+
// Built before the connector so the dispatcher can close over it: the
|
|
98
|
+
// connection needs an elicit handler at construction time, but which
|
|
99
|
+
// session that handler defers to is only knowable per request.
|
|
100
|
+
const created = {
|
|
101
|
+
connector: undefined,
|
|
102
|
+
callers: new Set(),
|
|
103
|
+
connected: undefined,
|
|
104
|
+
};
|
|
105
|
+
// A headless entry gets no dispatcher at all, so its connection declares
|
|
106
|
+
// no elicitation capability — matching what its callers can actually do.
|
|
107
|
+
created.connector = createConnector({
|
|
108
|
+
...opts,
|
|
109
|
+
onElicit: opts.onElicit ? this.dispatchElicit(created) : undefined,
|
|
110
|
+
// A pooled child that dies would otherwise be handed to every session
|
|
111
|
+
// in the daemon, and to every session that connects later — one crash
|
|
112
|
+
// becoming a permanent outage. Unregister it so the next acquire builds
|
|
113
|
+
// a fresh connection. Sessions already holding it still see their calls
|
|
114
|
+
// fail, which is what a dead server means; they recover on reconnect.
|
|
115
|
+
onClosed: () => this.evictDead(key, created),
|
|
116
|
+
});
|
|
117
|
+
created.connected = created.connector.connect(config);
|
|
118
|
+
entry = created;
|
|
119
|
+
this.entries.set(key, entry);
|
|
120
|
+
log("INFO", "mcp", `Opening shared MCP connection for "${config.name}"`);
|
|
121
|
+
}
|
|
122
|
+
entry.callers.add(caller);
|
|
123
|
+
const claimed = entry;
|
|
124
|
+
let result;
|
|
125
|
+
try {
|
|
126
|
+
result = await claimed.connected;
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
// The connector reports failure in-band, so a throw is unexpected. Drop
|
|
130
|
+
// the claim so a broken entry cannot outlive it.
|
|
131
|
+
await this.releaseKey(key, claimed, caller);
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
if (!result.ok) {
|
|
135
|
+
// A failed connection is not worth pooling: keeping it would make every
|
|
136
|
+
// later session inherit this failure with no chance to retry. Release the
|
|
137
|
+
// claim (tearing the entry down at zero) and report the failure as-is.
|
|
138
|
+
await this.releaseKey(key, claimed, caller);
|
|
139
|
+
return { result, release: async () => { }, beginCall: () => () => { } };
|
|
140
|
+
}
|
|
141
|
+
let released = false;
|
|
142
|
+
return {
|
|
143
|
+
result,
|
|
144
|
+
release: async () => {
|
|
145
|
+
if (released)
|
|
146
|
+
return;
|
|
147
|
+
released = true;
|
|
148
|
+
await this.releaseKey(key, claimed, caller);
|
|
149
|
+
},
|
|
150
|
+
beginCall: () => {
|
|
151
|
+
caller.activeCalls += 1;
|
|
152
|
+
let ended = false;
|
|
153
|
+
return () => {
|
|
154
|
+
if (ended)
|
|
155
|
+
return;
|
|
156
|
+
ended = true;
|
|
157
|
+
caller.activeCalls = Math.max(0, caller.activeCalls - 1);
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Drop a connection whose server died, without touching its callers' claims.
|
|
164
|
+
*
|
|
165
|
+
* The entry is unregistered but NOT disposed: its transport is already gone,
|
|
166
|
+
* and the sessions still holding handles will release them normally on their
|
|
167
|
+
* own dispose. Guarded on identity so a death notice arriving after the entry
|
|
168
|
+
* was already replaced cannot evict its successor.
|
|
169
|
+
*/
|
|
170
|
+
evictDead(key, entry) {
|
|
171
|
+
if (this.entries.get(key) !== entry)
|
|
172
|
+
return;
|
|
173
|
+
this.entries.delete(key);
|
|
174
|
+
log("WARN", "mcp", "shared MCP connection died; next session will reconnect");
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Route an elicitation to the session that provoked it.
|
|
178
|
+
*
|
|
179
|
+
* A shared connection is held by many windows, so "ask the user" has to name
|
|
180
|
+
* one. Elicitation only happens while a server is servicing a tool call, so
|
|
181
|
+
* the session with a call in flight IS the one whose window should show the
|
|
182
|
+
* form. When that is ambiguous — no call in flight, or two windows calling
|
|
183
|
+
* the same server at once — the request is cancelled rather than guessed:
|
|
184
|
+
* showing one project's consent form in another project's window would be a
|
|
185
|
+
* worse failure than a declined prompt, and `cancel` is exactly what a server
|
|
186
|
+
* already handles for a user who dismissed the dialog.
|
|
187
|
+
*/
|
|
188
|
+
dispatchElicit(entry) {
|
|
189
|
+
return async (request) => {
|
|
190
|
+
const candidates = [...entry.callers].filter((c) => c.activeCalls > 0 && c.onElicit);
|
|
191
|
+
if (candidates.length !== 1) {
|
|
192
|
+
log("WARN", "mcp", "cancelled a shared-server elicitation with no unique caller", {
|
|
193
|
+
server: request.server,
|
|
194
|
+
candidates: String(candidates.length),
|
|
195
|
+
});
|
|
196
|
+
return { action: "cancel" };
|
|
197
|
+
}
|
|
198
|
+
return candidates[0].onElicit(request);
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Drop one session's claim. At zero the entry is unregistered SYNCHRONOUSLY
|
|
203
|
+
* before the disposal await, so an `acquire` landing mid-teardown builds a
|
|
204
|
+
* fresh connection instead of receiving a client that is already closing.
|
|
205
|
+
*/
|
|
206
|
+
async releaseKey(key, entry, caller) {
|
|
207
|
+
if (!entry.callers.delete(caller))
|
|
208
|
+
return;
|
|
209
|
+
if (entry.callers.size > 0)
|
|
210
|
+
return;
|
|
211
|
+
// Only unregister when this entry is still the live one for the key: a
|
|
212
|
+
// teardown racing a fresh acquire must not evict its replacement.
|
|
213
|
+
if (this.entries.get(key) === entry)
|
|
214
|
+
this.entries.delete(key);
|
|
215
|
+
await entry.connector.dispose();
|
|
216
|
+
log("INFO", "mcp", "Closed shared MCP connection (last session released it)");
|
|
217
|
+
}
|
|
218
|
+
/** Live shared connections. Test/diagnostic use. */
|
|
219
|
+
get size() {
|
|
220
|
+
return this.entries.size;
|
|
221
|
+
}
|
|
222
|
+
/** Reference count for one config, or 0 when nothing is pooled for it. */
|
|
223
|
+
refCount(config, opts = {}) {
|
|
224
|
+
return this.entries.get(this.keyFor(config, opts))?.callers.size ?? 0;
|
|
225
|
+
}
|
|
226
|
+
/** Tear every shared connection down regardless of refs (process shutdown). */
|
|
227
|
+
async disposeAll() {
|
|
228
|
+
const entries = [...this.entries.values()];
|
|
229
|
+
this.entries.clear();
|
|
230
|
+
await Promise.all(entries.map((entry) => entry.connector.dispose()));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** The daemon-wide pool. One per process, which is exactly the sharing scope. */
|
|
234
|
+
export const sharedMcpPool = new SharedMcpPool();
|
|
235
|
+
//# sourceMappingURL=shared-pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-pool.js","sourceRoot":"","sources":["../../../src/core/mcp/shared-pool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-pool.test.d.ts","sourceRoot":"","sources":["../../../src/core/mcp/shared-pool.test.ts"],"names":[],"mappings":""}
|