char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Sommcp: manage lazy clients by environment lifetime and server configurationac8e0a3

main
6.5 KiB187 linesraw
1import type { Environment } from "@berry/api";
2import * as posix from "@std/path/posix";
3import * as windows from "@std/path/windows";
4import {
5  McpClient,
6  McpConnectionClosedError,
7  McpSessionExpiredError,
8  StreamableHttpTransport,
9} from "@earendil-works/pi-mcp";
10import type { Server } from "./config.ts";
11import { EnvironmentStdio } from "./stdio.ts";
12
13type Connection = {
14  id: string;
15  initialization: ReturnType<typeof setTimeout>;
16  client: McpClient;
17  ready: Promise<unknown>;
18  stop: AbortController;
19  backend?: AbortSignal;
20  disconnected: () => void;
21  stdio?: EnvironmentStdio;
22  failure?: Error;
23  retired: boolean;
24  users: number;
25  idle?: ReturnType<typeof setTimeout>;
26  closing?: Promise<void>;
27};
28
29/** Lazy clients shared only within the same machine, working directory, and resolved configuration. */
30export class Connections {
31  private readonly clients = new Map<string, Connection>();
32  private readonly owned = new Set<Connection>();
33  private disposed = false;
34
35  private close(connection: Connection): Promise<void> {
36    return connection.closing ??= (async () => {
37      connection.retired = true;
38      clearTimeout(connection.idle);
39      clearTimeout(connection.initialization);
40      if (this.clients.get(connection.id) === connection) this.clients.delete(connection.id);
41      connection.backend?.removeEventListener("abort", connection.disconnected);
42      try {
43        await connection.client.close();
44      } finally {
45        connection.stop.abort();
46        this.owned.delete(connection);
47      }
48    })();
49  }
50
51  async use<T>(
52    env: Environment,
53    name: string,
54    config: Server,
55    signal: AbortSignal,
56    run: (client: McpClient) => Promise<T>,
57  ): Promise<T> {
58    signal.throwIfAborted();
59    env.lifetime?.throwIfAborted();
60    if (this.disposed) throw new Error("MCP extension has been disposed");
61    if (!config.enabled) throw new Error(`MCP server "${name}" is disabled`);
62    const id = JSON.stringify([env.id, env.cwd, name, config]);
63    let connection = this.clients.get(id);
64    if (
65      !connection || connection.retired || connection.client.connectionState === "closed" ||
66      connection.backend !== env.lifetime
67    ) {
68      const previous = connection;
69      if (previous) previous.retired = true;
70      const stop = new AbortController();
71      const path = env.platform === "win32" ? windows : posix;
72      const client = new McpClient({
73        name: "berry",
74        version: "1",
75        requestTimeoutMs: config.timeout,
76        roots: [{ uri: path.toFileUrl(config.cwd).href, name: "workspace" }],
77      });
78      let stdio: EnvironmentStdio | undefined;
79      const transport = "command" in config
80        ? (stdio = new EnvironmentStdio(env, config, stop.signal))
81        : new StreamableHttpTransport({
82          url: config.url,
83          headers: config.headers,
84          fetch: async (input, init) => {
85            try {
86              return await env.fetch(input, {
87                ...init,
88                redirect: "error",
89                signal: AbortSignal.any([
90                  stop.signal,
91                  ...(env.lifetime ? [env.lifetime] : []),
92                  ...(init?.signal ? [init.signal] : []),
93                ]),
94              });
95            } catch (error) {
96              if (error instanceof TypeError) created.retired = true;
97              throw error;
98            }
99          },
100        });
101      const initializationTimeout = Math.min(config.timeout, 30_000);
102      const timer = setTimeout(() => {
103        created.failure = new Error(
104          `MCP server "${name}" did not initialize within ${initializationTimeout} ms`,
105        );
106        void this.close(created);
107      }, initializationTimeout);
108      const ready = (previous && previous.users === 0 ? this.close(previous) : Promise.resolve())
109        .then(() => {
110          stop.signal.throwIfAborted();
111          return client.connect(transport);
112        }).finally(() => clearTimeout(timer));
113      const disconnected = () => {
114        created.failure = env.lifetime?.reason;
115        void this.close(created);
116      };
117      const created: Connection = {
118        id,
119        initialization: timer,
120        client,
121        ready,
122        stop,
123        backend: env.lifetime,
124        disconnected,
125        stdio,
126        retired: false,
127        users: 0,
128      };
129      connection = created;
130      this.clients.set(id, created);
131      this.owned.add(created);
132      env.lifetime?.addEventListener("abort", disconnected, { once: true });
133      if (env.lifetime?.aborted) disconnected();
134      void ready.catch(() => {});
135    }
136    const current = connection;
137    clearTimeout(current.idle);
138    current.users++;
139    try {
140      await new Promise<void>((resolve, reject) => {
141        const finish = (error?: unknown) => {
142          signal.removeEventListener("abort", abort);
143          current.stop.signal.removeEventListener("abort", stopped);
144          if (error) reject(error);
145          else resolve();
146        };
147        const abort = () => finish(signal.reason);
148        const stopped = () => finish(current.failure ?? new McpConnectionClosedError());
149        signal.addEventListener("abort", abort, { once: true });
150        current.stop.signal.addEventListener("abort", stopped, { once: true });
151        void current.ready.then(() => finish(), finish);
152        if (signal.aborted) abort();
153        else if (current.stop.signal.aborted) stopped();
154      });
155      signal.throwIfAborted();
156      return await run(current.client);
157    } catch (error) {
158      // retire a failed session without replaying calls or interrupting its other active users
159      if (error instanceof McpSessionExpiredError) current.retired = true;
160      if (error instanceof McpConnectionClosedError && current.stdio) await current.stdio.close();
161      signal.throwIfAborted();
162      if (current.failure) throw current.failure;
163      if (error instanceof McpConnectionClosedError && current.stdio?.failure) {
164        throw current.stdio.failure;
165      }
166      throw error;
167    } finally {
168      current.users--;
169      if (current.users === 0) {
170        if (current.retired || current.client.connectionState === "closed") {
171          void this.close(current);
172        } else if (!this.disposed && !current.stop.signal.aborted) {
173          current.idle = setTimeout(() => {
174            if (this.clients.get(id) === current) this.clients.delete(id);
175            void this.close(current);
176          }, 5 * 60_000);
177        }
178      }
179    }
180  }
181
182  async dispose() {
183    this.disposed = true;
184    this.clients.clear();
185    await Promise.all([...this.owned].map((connection) => this.close(connection)));
186  }
187}