@yawlabs/ssh-mcp 0.16.1 → 0.17.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/ops.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Client } from "ssh2";
2
- import type { ConnectionPool } from "./pool.js";
2
+ import { type ConnectionPool } from "./pool.js";
3
3
  export declare function shellQuote(s: string): string;
4
4
  export interface MultiExecResult {
5
5
  host: string;
package/dist/pool.d.ts CHANGED
@@ -4,14 +4,49 @@ export interface PoolOptions {
4
4
  /** Milliseconds before an idle connection is closed. Default: 60000 (60s) */
5
5
  idleTtlMs?: number;
6
6
  /**
7
- * Maximum number of connections in the pool. Default: 100, overridable via the
7
+ * Maximum number of connections in the pool, counting dials still in flight.
8
+ * Default: 100, overridable via the
8
9
  * `SSH_MCP_MAX_POOL_SIZE` env var. When at capacity, the pool first tries to evict
9
- * an idle entry; if every entry is in use, `acquire()` rejects with
10
- * "Connection pool is full". Bump this for fan-out workloads against many distinct
11
- * hosts (e.g. `ssh_multi_exec` across a large fleet).
10
+ * an idle entry; if no idle entry can be evicted (every slot is in use or dialing),
11
+ * `acquire()` rejects with a {@link PoolFullError} ("Connection pool is full") -- at once
12
+ * by default, or once `waitForCapacityMs` (see {@link AcquireOptions}) passes without winning a slot.
13
+ * Bump this for fan-out workloads against many distinct hosts (e.g. `ssh_multi_exec`
14
+ * across a large fleet, which runs at most this many hosts at once).
12
15
  */
13
16
  maxPoolSize?: number;
14
17
  }
18
+ export interface AcquireOptions {
19
+ /**
20
+ * How long `acquire()` / `withConnection()` may wait for a slot when the pool is full,
21
+ * in milliseconds. Default 0: reject at once with a {@link PoolFullError}. With a budget,
22
+ * the caller parks (see `waitForCapacity`) and retries on every capacity signal until a
23
+ * slot is won or the budget is spent, then rejects with a {@link PoolFullError} whose
24
+ * message says "no slot became available to this call within <budget>ms". Only a capacity rejection is
25
+ * waited out; connect failures and a drained pool reject at once as before.
26
+ */
27
+ waitForCapacityMs?: number;
28
+ }
29
+ /** `code` of the error `acquire()` throws when no slot is free. Match on this, not on the message. */
30
+ export declare const POOL_FULL_ERROR_CODE = "ERR_SSH_MCP_POOL_FULL";
31
+ /**
32
+ * Thrown by `acquire()` when a new connection is needed and no slot is free: every slot is
33
+ * held by an in-use entry or an in-flight dial, and no idle entry can be evicted. Distinct
34
+ * from connect and exec failures so a caller that can wait (see `waitForCapacity`) retries
35
+ * only on this. `waitedMs` is set when the caller asked to wait and the budget ran out; the
36
+ * message then carries the "no slot became available to this call within <n>ms" suffix and a
37
+ * remedy sentence -- appended here, once, so no caller has to add it.
38
+ */
39
+ export declare class PoolFullError extends Error {
40
+ readonly code = "ERR_SSH_MCP_POOL_FULL";
41
+ readonly maxPoolSize: number;
42
+ readonly waitedMs?: number;
43
+ constructor(maxPoolSize: number, waitedMs?: number);
44
+ }
45
+ /**
46
+ * True for the pool's capacity rejection. Checks `code` rather than `instanceof`, so it still
47
+ * holds when two copies of this module are loaded (the CLI and library bundles are separate).
48
+ */
49
+ export declare function isPoolFullError(err: unknown): err is PoolFullError;
15
50
  export declare class ConnectionPool {
16
51
  private entries;
17
52
  private pending;
@@ -19,12 +54,46 @@ export declare class ConnectionPool {
19
54
  private maxPoolSize;
20
55
  private _connectCount;
21
56
  private drained;
57
+ private capacityWaiters;
22
58
  constructor(options?: PoolOptions);
23
- acquire(config: SSHConfig): Promise<Client>;
59
+ /**
60
+ * Checks out a connection for `config`, dialing one if needed. Fails fast on a full pool
61
+ * unless `options.waitForCapacityMs` is set, in which case the caller parks and retries on
62
+ * every capacity signal until it wins a slot or the budget is spent. The wait is bounded,
63
+ * never a reservation: a woken caller can lose the slot to another and simply parks again
64
+ * for whatever budget is left. The final rejection carries the budget in its message.
65
+ */
66
+ acquire(config: SSHConfig, options?: AcquireOptions): Promise<Client>;
67
+ private acquireNow;
24
68
  release(client: Client): void;
25
- withConnection<T>(config: SSHConfig, fn: (client: Client) => Promise<T>): Promise<T>;
69
+ /** `acquire()` + `fn` + `release()`, with the same optional bounded wait for a slot. */
70
+ withConnection<T>(config: SSHConfig, fn: (client: Client) => Promise<T>, options?: AcquireOptions): Promise<T>;
26
71
  drain(): void;
72
+ /**
73
+ * Parks until the pool's capacity may have changed, for a caller whose `acquire()` just
74
+ * rejected with a {@link PoolFullError} and that would rather wait than fail. This is the
75
+ * primitive behind `acquire()`'s `waitForCapacityMs` option; a bare `acquire()` never waits.
76
+ *
77
+ * Resolves `true` when a slot may be free (woken by a release, an entry closing or expiring,
78
+ * a failed dial, or `drain()`), or at once if a new host would fit right now or the pool is
79
+ * already drained. Resolves `false` after `timeoutMs` with nothing having changed. Never
80
+ * rejects. A `true` is a hint, not a reservation: another caller can take the slot first,
81
+ * so retry `acquire()` and wait again on another `PoolFullError`. The timer is cleared on
82
+ * wake and unref'd, so a parked caller never holds the process open.
83
+ *
84
+ * Aborting `signal` ends the park early with `false`, as if the timer had run out; an
85
+ * already-aborted signal resolves `false` without parking, unless the pool is drained or a
86
+ * new host would fit right now. A wake that lands first still resolves `true`. A caller that
87
+ * gives up on its own (`ssh_multi_exec` declaring the call starved) aborts rather than racing
88
+ * this promise against another: the race would add a promise hop to every wake.
89
+ */
90
+ waitForCapacity(timeoutMs: number, signal?: AbortSignal): Promise<boolean>;
91
+ private hasCapacity;
92
+ private findEvictable;
93
+ private notifyCapacity;
27
94
  get size(): number;
95
+ /** The connection cap (`maxPoolSize`), counting pooled entries plus in-flight dials. */
96
+ get maxSize(): number;
28
97
  get stats(): {
29
98
  active: number;
30
99
  idle: number;
package/dist/server.d.ts CHANGED
@@ -10,8 +10,8 @@ export type { FindOptions, MultiExecHost, MultiExecResult, ServiceStatus } from
10
10
  export { find, multiExec, serviceStatus, tail } from "./ops.js";
11
11
  export type { PolicyContext } from "./policy.js";
12
12
  export { enforcePolicy, isPolicyConfigured } from "./policy.js";
13
- export type { PoolOptions } from "./pool.js";
14
- export { ConnectionPool } from "./pool.js";
13
+ export type { AcquireOptions, PoolOptions } from "./pool.js";
14
+ export { ConnectionPool, isPoolFullError, POOL_FULL_ERROR_CODE, PoolFullError } from "./pool.js";
15
15
  export type { ExecResult, FileStats, HostKeyRejection, HostKeyRejectionReason, ResolvedConfig, SSHConfig, } from "./ssh.js";
16
16
  export { connect, connectRaw, connectWithProxy, deleteFile, downloadFile, exec, formatDiagnostics, listDir, makeDir, readFile, readKnownHostsKeys, resolveConfig, statFile, uploadFile, writeFile, } from "./ssh.js";
17
17
  export { registerTools } from "./tools.js";