@wrongstack/mcp 1.0.10 → 1.0.11

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/index.js CHANGED
@@ -5,6 +5,14 @@ import * as http from "node:http";
5
5
  import * as https from "node:https";
6
6
  import * as net from "node:net";
7
7
  import { isPrivateIPv4, isPrivateIPv6 } from "@wrongstack/core/utils";
8
+ var MCPOAuthHttpError = class extends Error {
9
+ constructor(message, status) {
10
+ super(message);
11
+ this.status = status;
12
+ }
13
+ status;
14
+ name = "MCPOAuthHttpError";
15
+ };
8
16
  function canonicalMcpResource(rawUrl) {
9
17
  let url;
10
18
  try {
@@ -299,7 +307,8 @@ async function exchangeMcpAuthorizationCode(options) {
299
307
  timeoutMs: options.timeoutMs,
300
308
  maxResponseBytes: options.maxResponseBytes,
301
309
  lookup: options.lookup,
302
- allowedLoopbackHostname: loopbackHostnameForResource(resource)
310
+ allowedLoopbackHostname: loopbackHostnameForResource(resource),
311
+ label: "token endpoint"
303
312
  });
304
313
  if (response === void 0) throw new Error("MCP OAuth token endpoint returned no response");
305
314
  return parseTokenResponse(response, resource);
@@ -321,7 +330,8 @@ async function refreshMcpAccessToken(options) {
321
330
  timeoutMs: options.timeoutMs,
322
331
  maxResponseBytes: options.maxResponseBytes,
323
332
  lookup: options.lookup,
324
- allowedLoopbackHostname: loopbackHostnameForResource(resource)
333
+ allowedLoopbackHostname: loopbackHostnameForResource(resource),
334
+ label: "token endpoint"
325
335
  });
326
336
  if (response === void 0) throw new Error("MCP OAuth token endpoint returned no response");
327
337
  const parsed = parseTokenResponse(response, resource);
@@ -412,7 +422,8 @@ async function discoverFirst(candidates, fetchJson, signal, parse, label) {
412
422
  throw new Error(`MCP ${label} discovery failed (${failures.join("; ")})`);
413
423
  }
414
424
  async function requestPinnedJson(rawUrl, options) {
415
- const url = secureOAuthUrl(rawUrl, "discovery URL");
425
+ const label = options.label ?? "discovery";
426
+ const url = secureOAuthUrl(rawUrl, `${label} URL`);
416
427
  const target = await resolvePinnedAddress(url, options);
417
428
  const timeoutMs = options.timeoutMs ?? 1e4;
418
429
  const maxBytes = options.maxResponseBytes ?? 64 * 1024;
@@ -456,12 +467,12 @@ async function requestPinnedJson(rawUrl, options) {
456
467
  }
457
468
  if (status >= 300 && status < 400) {
458
469
  response.resume();
459
- finish(new Error("MCP OAuth discovery redirects are not allowed"));
470
+ finish(new Error(`MCP OAuth ${label} redirects are not allowed`));
460
471
  return;
461
472
  }
462
473
  if (status < 200 || status >= 300) {
463
474
  response.resume();
464
- finish(new Error(`MCP OAuth discovery HTTP ${status}`));
475
+ finish(new MCPOAuthHttpError(`MCP OAuth ${label} HTTP ${status}`, status));
465
476
  return;
466
477
  }
467
478
  const contentType = response.headers["content-type"] ?? "";
@@ -1127,6 +1138,34 @@ function parseGetPromptResult(value) {
1127
1138
  }
1128
1139
 
1129
1140
  // src/tool-schema.ts
1141
+ var MAX_TOOL_PAGES = 100;
1142
+ var MAX_TOOLS = 1e4;
1143
+ async function listAllTools(requestPage) {
1144
+ const tools = [];
1145
+ const seenCursors = /* @__PURE__ */ new Set();
1146
+ let cursor;
1147
+ for (let page = 0; page < MAX_TOOL_PAGES; page++) {
1148
+ let response;
1149
+ try {
1150
+ response = await requestPage(cursor ? { cursor } : {});
1151
+ } catch (err) {
1152
+ if (page === 0) throw err;
1153
+ break;
1154
+ }
1155
+ if (response.error) {
1156
+ if (page === 0) return null;
1157
+ break;
1158
+ }
1159
+ const result = response.result;
1160
+ tools.push(...normalizeMCPTools(result?.tools));
1161
+ const next = result?.nextCursor;
1162
+ if (typeof next !== "string" || next.length === 0) break;
1163
+ if (seenCursors.has(next) || tools.length >= MAX_TOOLS) break;
1164
+ seenCursors.add(next);
1165
+ cursor = next;
1166
+ }
1167
+ return tools.slice(0, MAX_TOOLS);
1168
+ }
1130
1169
  function normalizeMCPTools(value) {
1131
1170
  if (!Array.isArray(value)) return [];
1132
1171
  const tools = [];
@@ -1164,6 +1203,20 @@ var SSE_READER_MAX_DATA_LINES = 1024;
1164
1203
  var SSEReader = class {
1165
1204
  buffer = "";
1166
1205
  dataLines = [];
1206
+ eventName = "";
1207
+ endpointListeners = [];
1208
+ /**
1209
+ * Legacy HTTP+SSE transport: the server's first event is
1210
+ * `event: endpoint` whose data is the (relative) URL to POST requests to.
1211
+ * Its payload is a URL, not JSON, so it is dispatched separately.
1212
+ */
1213
+ onEndpoint(cb) {
1214
+ this.endpointListeners.push(cb);
1215
+ return () => {
1216
+ const idx = this.endpointListeners.indexOf(cb);
1217
+ if (idx >= 0) this.endpointListeners.splice(idx, 1);
1218
+ };
1219
+ }
1167
1220
  listeners = [];
1168
1221
  onMessage(cb) {
1169
1222
  this.listeners.push(cb);
@@ -1216,6 +1269,7 @@ var SSEReader = class {
1216
1269
  let value = colonIdx === -1 ? "" : line.slice(colonIdx + 1);
1217
1270
  if (value.startsWith(" ")) value = value.slice(1);
1218
1271
  if (field === "event") {
1272
+ this.eventName = value;
1219
1273
  } else if (field === "data") {
1220
1274
  if (this.dataLines.length >= SSE_READER_MAX_DATA_LINES) {
1221
1275
  throw new ToolError({
@@ -1233,12 +1287,23 @@ var SSEReader = class {
1233
1287
  }
1234
1288
  }
1235
1289
  flush() {
1290
+ const eventName = this.eventName;
1291
+ this.eventName = "";
1236
1292
  if (this.dataLines.length === 0) {
1237
1293
  return;
1238
1294
  }
1239
1295
  const data = this.dataLines.join("\n").trim();
1240
1296
  this.dataLines = [];
1241
1297
  if (!data) return;
1298
+ if (eventName === "endpoint") {
1299
+ for (const cb of this.endpointListeners) {
1300
+ try {
1301
+ cb(data);
1302
+ } catch {
1303
+ }
1304
+ }
1305
+ return;
1306
+ }
1242
1307
  try {
1243
1308
  const parsed = JSON.parse(data);
1244
1309
  this.dispatch(parsed);
@@ -1256,7 +1321,9 @@ var SSEReader = class {
1256
1321
  reset() {
1257
1322
  this.buffer = "";
1258
1323
  this.dataLines = [];
1324
+ this.eventName = "";
1259
1325
  this.listeners = [];
1326
+ this.endpointListeners = [];
1260
1327
  }
1261
1328
  };
1262
1329
 
@@ -1688,6 +1755,11 @@ var BaseHTTPTransport = class {
1688
1755
 
1689
1756
  // src/transport-jsonrpc.ts
1690
1757
  import { ToolError as ToolError2 } from "@wrongstack/core/types";
1758
+ function encodeJsonRpcMessage(id, method, params) {
1759
+ return JSON.stringify(
1760
+ method.startsWith("notifications/") ? { jsonrpc: "2.0", method, params } : { jsonrpc: "2.0", id, method, params }
1761
+ );
1762
+ }
1691
1763
  function isJsonRpcResult(v) {
1692
1764
  if (typeof v !== "object" || v === null) return false;
1693
1765
  const r = v;
@@ -1813,11 +1885,20 @@ async function readBodyCapped(res, maxBytes = MAX_MCP_HTTP_BODY_BYTES) {
1813
1885
  }
1814
1886
 
1815
1887
  // src/transport-sse.ts
1888
+ var ENDPOINT_WAIT_MS = 1e3;
1889
+ var SESSION_FATAL_HTTP_STATUSES = /* @__PURE__ */ new Set([401, 403, 404, 410]);
1816
1890
  var SSETransport = class extends BaseHTTPTransport {
1817
1891
  _nextId = 1;
1818
1892
  readerDone = false;
1893
+ closed = false;
1819
1894
  readLoopAbort;
1820
1895
  reader;
1896
+ /** POST target announced by the server's `endpoint` event. */
1897
+ endpointUrl;
1898
+ /** Wakes connect() once the endpoint is known (or will not arrive). */
1899
+ streamSignal;
1900
+ /** Requests whose response is expected over the event stream, keyed by id. */
1901
+ streamPending = /* @__PURE__ */ new Map();
1821
1902
  constructor(opts) {
1822
1903
  super(opts, "SSETransport");
1823
1904
  }
@@ -1829,18 +1910,13 @@ var SSETransport = class extends BaseHTTPTransport {
1829
1910
  /** Refresh tool list when server sends notifications/tools/list_changed. */
1830
1911
  async handleToolsListChanged() {
1831
1912
  try {
1832
- const res = await this.httpPost("tools/list", {});
1833
- if (!res.error) {
1834
- this.tools.splice(
1835
- 0,
1836
- this.tools.length,
1837
- ...normalizeMCPTools(res.result?.tools)
1838
- );
1839
- for (const cb of this.toolsChangedListeners) {
1840
- try {
1841
- cb([...this.tools]);
1842
- } catch {
1843
- }
1913
+ const tools = await listAllTools((params) => this.httpPost("tools/list", params));
1914
+ if (!tools) return;
1915
+ this.tools.splice(0, this.tools.length, ...tools);
1916
+ for (const cb of this.toolsChangedListeners) {
1917
+ try {
1918
+ cb([...this.tools]);
1919
+ } catch {
1844
1920
  }
1845
1921
  }
1846
1922
  } catch {
@@ -1848,6 +1924,8 @@ var SSETransport = class extends BaseHTTPTransport {
1848
1924
  }
1849
1925
  async connect() {
1850
1926
  this.readerDone = false;
1927
+ this.closed = false;
1928
+ this.endpointUrl = void 0;
1851
1929
  this.state = "connecting";
1852
1930
  this.serverMetadata = void 0;
1853
1931
  this.abortController = new AbortController();
@@ -1856,7 +1934,7 @@ var SSETransport = class extends BaseHTTPTransport {
1856
1934
  try {
1857
1935
  const sseUrl = this.buildSSEUrl();
1858
1936
  const fetchOpts = {
1859
- headers: this.headers,
1937
+ headers: { Accept: "text/event-stream", ...this.headers },
1860
1938
  signal
1861
1939
  };
1862
1940
  this.applyTlsAgent(fetchOpts);
@@ -1880,7 +1958,15 @@ var SSETransport = class extends BaseHTTPTransport {
1880
1958
  const textDecoder = new TextDecoder();
1881
1959
  const sseReader = new SSEReader();
1882
1960
  this.readLoopAbort = new AbortController();
1961
+ const streamReady = new Promise((resolve) => {
1962
+ this.streamSignal = resolve;
1963
+ });
1964
+ sseReader.onEndpoint((endpoint) => {
1965
+ this.acceptEndpoint(endpoint);
1966
+ this.streamSignal?.();
1967
+ });
1883
1968
  sseReader.onMessage((msg) => {
1969
+ this.streamSignal?.();
1884
1970
  if (msg.method && !msg.id) {
1885
1971
  if (msg.method === "notifications/tools/list_changed") {
1886
1972
  void this.handleToolsListChanged();
@@ -1889,6 +1975,14 @@ var SSETransport = class extends BaseHTTPTransport {
1889
1975
  } else if (msg.method === "notifications/prompts/list_changed") {
1890
1976
  this.notifyPromptsChanged();
1891
1977
  }
1978
+ return;
1979
+ }
1980
+ if (!msg.method && isJsonRpcResult(msg)) {
1981
+ const pending = this.streamPending.get(msg.id);
1982
+ if (pending) {
1983
+ this.streamPending.delete(msg.id);
1984
+ pending.resolve(msg);
1985
+ }
1892
1986
  }
1893
1987
  });
1894
1988
  const reader = response.body.getReader();
@@ -1896,10 +1990,13 @@ var SSETransport = class extends BaseHTTPTransport {
1896
1990
  cancel: () => reader.cancel(),
1897
1991
  releaseLock: () => reader.releaseLock()
1898
1992
  };
1899
- this.readSSEBody(reader, textDecoder, sseReader);
1993
+ void this.readSSEBody(reader, textDecoder, sseReader);
1994
+ await this.waitForStream(streamReady, signal);
1900
1995
  const initRes = await this.httpPost("initialize", {
1901
1996
  protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
1902
- capabilities: { tools: {} },
1997
+ // Client capabilities (roots/sampling/elicitation) none are offered.
1998
+ // `tools` is a SERVER capability and never belonged here.
1999
+ capabilities: {},
1903
2000
  clientInfo: MCP_CONSTANTS.CLIENT_INFO
1904
2001
  });
1905
2002
  if (initRes.error) {
@@ -1916,13 +2013,8 @@ var SSETransport = class extends BaseHTTPTransport {
1916
2013
  await this.httpPost("notifications/initialized", {});
1917
2014
  } catch {
1918
2015
  }
1919
- const toolsRes = await this.httpPost("tools/list", {});
1920
- if (toolsRes.error) {
1921
- this.tools.splice(0, this.tools.length);
1922
- } else {
1923
- const result = toolsRes.result;
1924
- this.tools.splice(0, this.tools.length, ...normalizeMCPTools(result?.tools));
1925
- }
2016
+ const tools = await listAllTools((params) => this.httpPost("tools/list", params));
2017
+ this.tools.splice(0, this.tools.length, ...tools ?? []);
1926
2018
  this.state = "connected";
1927
2019
  clearTimeout(startupTimer);
1928
2020
  } catch (err) {
@@ -1930,6 +2022,43 @@ var SSETransport = class extends BaseHTTPTransport {
1930
2022
  this.state = "failed";
1931
2023
  this.abortController.abort();
1932
2024
  throw err;
2025
+ } finally {
2026
+ this.streamSignal = void 0;
2027
+ }
2028
+ }
2029
+ /** Resolve once the stream announced its endpoint, emitted anything, ended, or the cap passed. */
2030
+ async waitForStream(ready, signal) {
2031
+ let timer;
2032
+ let onAbort;
2033
+ try {
2034
+ await Promise.race([
2035
+ ready,
2036
+ new Promise((resolve) => {
2037
+ timer = setTimeout(resolve, Math.min(ENDPOINT_WAIT_MS, this.timeout));
2038
+ timer.unref?.();
2039
+ }),
2040
+ new Promise((resolve) => {
2041
+ onAbort = resolve;
2042
+ signal.addEventListener("abort", onAbort, { once: true });
2043
+ })
2044
+ ]);
2045
+ } finally {
2046
+ clearTimeout(timer);
2047
+ if (onAbort) signal.removeEventListener("abort", onAbort);
2048
+ }
2049
+ }
2050
+ /**
2051
+ * Adopt the server's POST endpoint. It must stay on the configured origin:
2052
+ * a cross-origin endpoint would move every request — Authorization header
2053
+ * included — to a host that never passed configuration review.
2054
+ */
2055
+ acceptEndpoint(endpoint) {
2056
+ try {
2057
+ const next = new URL(endpoint, this.url);
2058
+ if (next.origin !== new URL(this.url).origin) return;
2059
+ validateTransportUrl(next.toString());
2060
+ this.endpointUrl = next.toString();
2061
+ } catch {
1933
2062
  }
1934
2063
  }
1935
2064
  async readSSEBody(reader, decoder, sseReader) {
@@ -1942,12 +2071,21 @@ var SSETransport = class extends BaseHTTPTransport {
1942
2071
  }
1943
2072
  } catch {
1944
2073
  } finally {
2074
+ this.streamSignal?.();
2075
+ this.rejectStreamPending("SSE stream closed");
1945
2076
  if (!this.readerDone && this.state !== "disconnected" && this.state !== "failed") {
1946
2077
  this.state = "disconnected";
1947
2078
  this.notifyDisconnect();
1948
2079
  }
1949
2080
  }
1950
2081
  }
2082
+ rejectStreamPending(reason) {
2083
+ if (this.streamPending.size === 0) return;
2084
+ const err = new Error(`MCP "${this.name}": ${reason}`);
2085
+ const pending = [...this.streamPending.values()];
2086
+ this.streamPending.clear();
2087
+ for (const entry of pending) entry.reject(err);
2088
+ }
1951
2089
  buildSSEUrl() {
1952
2090
  try {
1953
2091
  const url = new URL(this.url);
@@ -1957,12 +2095,37 @@ var SSETransport = class extends BaseHTTPTransport {
1957
2095
  return this.url;
1958
2096
  }
1959
2097
  }
1960
- async httpPost(method, params, opts) {
2098
+ /** Register interest in a stream-delivered response before the POST goes out. */
2099
+ awaitStreamResponse(id, signal) {
2100
+ const promise = new Promise((resolve, reject) => {
2101
+ const onAbort = () => reject(signal.reason instanceof Error ? signal.reason : new Error("MCP request aborted"));
2102
+ this.streamPending.set(id, {
2103
+ resolve: (result) => {
2104
+ signal.removeEventListener("abort", onAbort);
2105
+ resolve(result);
2106
+ },
2107
+ reject: (err) => {
2108
+ signal.removeEventListener("abort", onAbort);
2109
+ reject(err);
2110
+ }
2111
+ });
2112
+ if (signal.aborted) onAbort();
2113
+ else signal.addEventListener("abort", onAbort, { once: true });
2114
+ });
2115
+ promise.catch(() => void 0);
2116
+ return promise;
2117
+ }
2118
+ httpPost(method, params, opts) {
2119
+ return this.postJsonRpc(method, params, this.requestTimeout, opts);
2120
+ }
2121
+ async postJsonRpc(method, params, timeoutMs, opts) {
1961
2122
  const id = this.genId();
1962
- const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
2123
+ const isNotification = method.startsWith("notifications/");
2124
+ const body = encodeJsonRpcMessage(id, method, params);
1963
2125
  const external = opts?.signal;
1964
2126
  const parent = external && this.abortController ? AbortSignal.any([this.abortController.signal, external]) : external ?? this.abortController?.signal;
1965
- const timeoutSignal = createTimeoutSignal(parent, this.requestTimeout);
2127
+ const timeoutSignal = createTimeoutSignal(parent, timeoutMs);
2128
+ const streamed = isNotification ? void 0 : this.awaitStreamResponse(id, timeoutSignal.signal);
1966
2129
  const fetchOpts = {
1967
2130
  method: "POST",
1968
2131
  headers: {
@@ -1974,8 +2137,19 @@ var SSETransport = class extends BaseHTTPTransport {
1974
2137
  };
1975
2138
  this.applyTlsAgent(fetchOpts);
1976
2139
  try {
1977
- const res = await this.fetchWithAuthorization(this.url, fetchOpts, timeoutSignal.signal);
2140
+ let res;
2141
+ try {
2142
+ res = await this.fetchWithAuthorization(
2143
+ this.endpointUrl ?? this.url,
2144
+ fetchOpts,
2145
+ timeoutSignal.signal
2146
+ );
2147
+ } catch (err) {
2148
+ if (!timeoutSignal.signal.aborted) this.markDisconnected();
2149
+ throw err;
2150
+ }
1978
2151
  if (!res.ok) {
2152
+ if (SESSION_FATAL_HTTP_STATUSES.has(res.status)) this.markDisconnected();
1979
2153
  let snippet;
1980
2154
  try {
1981
2155
  snippet = await readBodyCapped(res, MCP_CONSTANTS.REQUEST_LOG_CAP);
@@ -1990,25 +2164,28 @@ var SSETransport = class extends BaseHTTPTransport {
1990
2164
  context: { transport: "sse", url: this.url, status: res.status }
1991
2165
  });
1992
2166
  }
1993
- if (method.startsWith("notifications/")) {
2167
+ if (isNotification || !streamed) {
1994
2168
  await readBodyCapped(res).catch(() => void 0);
1995
2169
  return { jsonrpc: "2.0", id };
1996
2170
  }
2171
+ let text;
2172
+ try {
2173
+ text = await readBodyCapped(res);
2174
+ } catch (err) {
2175
+ throw invalidResponse(method, this.url, err);
2176
+ }
2177
+ if (res.status === 202 || text.trim() === "") {
2178
+ return await streamed;
2179
+ }
1997
2180
  let data;
1998
2181
  try {
1999
- data = JSON.parse(await readBodyCapped(res));
2182
+ data = JSON.parse(text);
2000
2183
  } catch (err) {
2001
- throw new ToolError4({
2002
- message: `Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
2003
- code: "TOOL_EXECUTION_FAILED",
2004
- toolName: method,
2005
- context: { transport: "sse", url: this.url, phase: "parse-json" },
2006
- cause: err
2007
- });
2184
+ throw invalidResponse(method, this.url, err);
2008
2185
  }
2009
2186
  return assertMatchingJsonRpcResult(data, id, method);
2010
2187
  } catch (err) {
2011
- if (external?.aborted && !method.startsWith("notifications/")) {
2188
+ if (external?.aborted && !isNotification) {
2012
2189
  void this.httpPost("notifications/cancelled", {
2013
2190
  requestId: id,
2014
2191
  reason: "client aborted"
@@ -2016,9 +2193,13 @@ var SSETransport = class extends BaseHTTPTransport {
2016
2193
  });
2017
2194
  throw makeAbortError(method);
2018
2195
  }
2019
- this.markDisconnected();
2020
2196
  throw err;
2021
2197
  } finally {
2198
+ const pending = this.streamPending.get(id);
2199
+ if (pending) {
2200
+ this.streamPending.delete(id);
2201
+ pending.reject(new Error("MCP request settled"));
2202
+ }
2022
2203
  timeoutSignal.dispose();
2023
2204
  }
2024
2205
  }
@@ -2043,73 +2224,16 @@ var SSETransport = class extends BaseHTTPTransport {
2043
2224
  }
2044
2225
  /** Generic JSON-RPC request — used by MCPClient.request() for SSE transports. */
2045
2226
  async request(method, params, timeoutMs, opts) {
2046
- const id = this.genId();
2047
- const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
2048
- const external = opts?.signal;
2049
- const parent = external && this.abortController ? AbortSignal.any([this.abortController.signal, external]) : external ?? this.abortController?.signal;
2050
- const timeoutSignal = createTimeoutSignal(parent, timeoutMs ?? this.requestTimeout);
2051
- const fetchOpts = {
2052
- method: "POST",
2053
- headers: {
2054
- "Content-Type": "application/json",
2055
- ...this.headers
2056
- },
2057
- body,
2058
- signal: timeoutSignal.signal
2059
- };
2060
- this.applyTlsAgent(fetchOpts);
2061
- try {
2062
- const res = await this.fetchWithAuthorization(this.url, fetchOpts, timeoutSignal.signal);
2063
- if (!res.ok) {
2064
- throw new ToolError4({
2065
- message: `HTTP ${res.status}: ${res.statusText}`,
2066
- code: "TOOL_EXECUTION_FAILED",
2067
- toolName: method,
2068
- context: {
2069
- transport: "sse",
2070
- url: this.url,
2071
- status: res.status,
2072
- statusText: res.statusText
2073
- }
2074
- });
2075
- }
2076
- if (method.startsWith("notifications/")) {
2077
- await readBodyCapped(res).catch(() => void 0);
2078
- return { jsonrpc: "2.0", id };
2079
- }
2080
- let data;
2081
- try {
2082
- data = JSON.parse(await readBodyCapped(res));
2083
- } catch (err) {
2084
- throw new ToolError4({
2085
- message: `Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
2086
- code: "TOOL_EXECUTION_FAILED",
2087
- toolName: method,
2088
- context: { transport: "sse", url: this.url, phase: "parse-json" },
2089
- cause: err
2090
- });
2091
- }
2092
- const result = assertMatchingJsonRpcResult(data, id, method);
2093
- return { jsonrpc: "2.0", id, result: result.result, error: result.error };
2094
- } catch (err) {
2095
- if (external?.aborted && !method.startsWith("notifications/")) {
2096
- void this.httpPost("notifications/cancelled", {
2097
- requestId: id,
2098
- reason: "client aborted"
2099
- }).catch(() => {
2100
- });
2101
- throw makeAbortError(method);
2102
- }
2103
- this.markDisconnected();
2104
- throw err;
2105
- } finally {
2106
- timeoutSignal.dispose();
2107
- }
2227
+ const result = await this.postJsonRpc(method, params, timeoutMs ?? this.requestTimeout, opts);
2228
+ return { jsonrpc: "2.0", id: result.id, result: result.result, error: result.error };
2108
2229
  }
2109
2230
  async close() {
2110
2231
  this.releasePinnedDispatcher();
2111
- if (this.state === "disconnected") return;
2232
+ const alreadyClosed = this.closed;
2233
+ this.closed = true;
2112
2234
  this.readerDone = true;
2235
+ this.state = "disconnected";
2236
+ if (alreadyClosed) return;
2113
2237
  this.readLoopAbort?.abort();
2114
2238
  try {
2115
2239
  this.reader?.cancel();
@@ -2120,8 +2244,8 @@ var SSETransport = class extends BaseHTTPTransport {
2120
2244
  } catch {
2121
2245
  }
2122
2246
  this.abortController?.abort();
2247
+ this.rejectStreamPending("transport closed");
2123
2248
  this.disconnectHandlers.splice(0, this.disconnectHandlers.length);
2124
- this.state = "disconnected";
2125
2249
  }
2126
2250
  markDisconnected() {
2127
2251
  if (this.state === "connected") {
@@ -2130,10 +2254,20 @@ var SSETransport = class extends BaseHTTPTransport {
2130
2254
  }
2131
2255
  }
2132
2256
  };
2257
+ function invalidResponse(method, url, err) {
2258
+ return new ToolError4({
2259
+ message: `Invalid JSON-RPC response: ${err instanceof Error ? err.message : "parse failed"}`,
2260
+ code: "TOOL_EXECUTION_FAILED",
2261
+ toolName: method,
2262
+ context: { transport: "sse", url, phase: "parse-json" },
2263
+ cause: err
2264
+ });
2265
+ }
2133
2266
 
2134
2267
  // src/transport-streamable.ts
2135
2268
  var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTransport {
2136
2269
  _nextId = 1;
2270
+ closed = false;
2137
2271
  sessionId;
2138
2272
  constructor(opts) {
2139
2273
  super(opts, "StreamableHTTP");
@@ -2164,11 +2298,8 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
2164
2298
  }
2165
2299
  async refreshTools() {
2166
2300
  try {
2167
- const response = await this.postRaw("tools/list", {});
2168
- if (response.error) return;
2169
- const tools = normalizeMCPTools(
2170
- response.result?.tools
2171
- );
2301
+ const tools = await listAllTools((params) => this.postRaw("tools/list", params));
2302
+ if (!tools) return;
2172
2303
  this.tools.splice(0, this.tools.length, ...tools);
2173
2304
  for (const listener of this.toolsChangedListeners) {
2174
2305
  try {
@@ -2180,6 +2311,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
2180
2311
  }
2181
2312
  }
2182
2313
  async connect() {
2314
+ this.closed = false;
2183
2315
  this.state = "connecting";
2184
2316
  this.serverMetadata = void 0;
2185
2317
  this.abortController = new AbortController();
@@ -2200,7 +2332,8 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
2200
2332
  method: "initialize",
2201
2333
  params: {
2202
2334
  protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
2203
- capabilities: { tools: {} },
2335
+ // Client capabilities none offered (`tools` is a server capability).
2336
+ capabilities: {},
2204
2337
  clientInfo: MCP_CONSTANTS.CLIENT_INFO
2205
2338
  }
2206
2339
  }),
@@ -2230,13 +2363,8 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
2230
2363
  this.protocolVersion = this.serverMetadata.protocolVersion;
2231
2364
  this.sessionId = initRes.headers.get("mcp-session-id") ?? void 0;
2232
2365
  await this.postRaw("notifications/initialized", {});
2233
- const toolsRes = await this.postRaw("tools/list", {});
2234
- if (toolsRes.error) {
2235
- this.tools.splice(0, this.tools.length);
2236
- } else {
2237
- const result = toolsRes.result;
2238
- this.tools.splice(0, this.tools.length, ...normalizeMCPTools(result?.tools));
2239
- }
2366
+ const tools = await listAllTools((params) => this.postRaw("tools/list", params));
2367
+ this.tools.splice(0, this.tools.length, ...tools ?? []);
2240
2368
  this.state = "connected";
2241
2369
  clearTimeout(startupTimer);
2242
2370
  } catch (err) {
@@ -2248,7 +2376,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
2248
2376
  }
2249
2377
  async postRaw(method, params, opts) {
2250
2378
  const id = this.genId();
2251
- const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
2379
+ const body = encodeJsonRpcMessage(id, method, params);
2252
2380
  const external = opts?.signal;
2253
2381
  const parent = external && this.abortController ? AbortSignal.any([this.abortController.signal, external]) : external ?? this.abortController?.signal;
2254
2382
  const timeoutSignal = createTimeoutSignal(parent, this.requestTimeout);
@@ -2298,7 +2426,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
2298
2426
  /** Generic JSON-RPC request — used by MCPClient.request() for SSE/streamable-http transports. */
2299
2427
  async request(method, params, timeoutMs, opts) {
2300
2428
  const id = this.genId();
2301
- const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
2429
+ const body = encodeJsonRpcMessage(id, method, params);
2302
2430
  const external = opts?.signal;
2303
2431
  const parent = external && this.abortController ? AbortSignal.any([this.abortController.signal, external]) : external ?? this.abortController?.signal;
2304
2432
  const timeoutSignal = createTimeoutSignal(parent, timeoutMs ?? this.requestTimeout);
@@ -2328,11 +2456,12 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
2328
2456
  }
2329
2457
  const parsed = this.consumeResponseText(await readBodyCapped(res), id);
2330
2458
  if (parsed) {
2459
+ const matched = assertMatchingJsonRpcResult(parsed, id, method);
2331
2460
  return {
2332
2461
  jsonrpc: "2.0",
2333
2462
  id,
2334
- result: parsed.result,
2335
- error: parsed.error
2463
+ result: matched.result,
2464
+ error: matched.error
2336
2465
  };
2337
2466
  }
2338
2467
  throw new Error("Could not parse response as JSON-RPC");
@@ -2366,8 +2495,10 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
2366
2495
  }
2367
2496
  async close() {
2368
2497
  this.releasePinnedDispatcher();
2369
- if (this.state === "disconnected") return;
2498
+ const alreadyClosed = this.closed;
2499
+ this.closed = true;
2370
2500
  this.state = "disconnected";
2501
+ if (alreadyClosed) return;
2371
2502
  this.abortController?.abort();
2372
2503
  this.disconnectHandlers.splice(0, this.disconnectHandlers.length);
2373
2504
  }
@@ -2525,12 +2656,18 @@ var MCPClient = class _MCPClient {
2525
2656
  return spawn2(shim.command, shim.args, {
2526
2657
  env: spawnEnv,
2527
2658
  stdio,
2659
+ ...this.opts.cwd ? { cwd: this.opts.cwd } : {},
2528
2660
  windowsVerbatimArguments: shim.windowsVerbatimArguments,
2529
2661
  // Without this every MCP server spawned from a console-less host
2530
2662
  // (WebUI server, scheduled runs) opens a visible console window.
2531
2663
  windowsHide: true
2532
2664
  });
2533
- })() : spawn2(this.opts.command, rawArgs, { env: spawnEnv, stdio, windowsHide: true });
2665
+ })() : spawn2(this.opts.command, rawArgs, {
2666
+ env: spawnEnv,
2667
+ stdio,
2668
+ windowsHide: true,
2669
+ ...this.opts.cwd ? { cwd: this.opts.cwd } : {}
2670
+ });
2534
2671
  this.child = child;
2535
2672
  child.stdout?.on("data", (chunk) => this.onData(chunk.toString()));
2536
2673
  child.stdout?.on("end", () => {
@@ -2565,7 +2702,9 @@ var MCPClient = class _MCPClient {
2565
2702
  "initialize",
2566
2703
  {
2567
2704
  protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
2568
- capabilities: { tools: {} },
2705
+ // Client capabilities (roots/sampling/elicitation) none offered.
2706
+ // `tools` is a SERVER capability and never belonged here.
2707
+ capabilities: {},
2569
2708
  clientInfo: MCP_CONSTANTS.CLIENT_INFO
2570
2709
  },
2571
2710
  this.opts.startupTimeoutMs ?? 1e4
@@ -2587,13 +2726,7 @@ var MCPClient = class _MCPClient {
2587
2726
  '[MCP] notify("notifications/initialized") failed for "' + this.opts.name + '": ' + toErrorMessage(err)
2588
2727
  );
2589
2728
  }
2590
- const toolsRes = await this.request("tools/list", {});
2591
- if (toolsRes.error) {
2592
- this._tools = [];
2593
- } else {
2594
- const result = toolsRes.result;
2595
- this._tools = normalizeMCPTools(result?.tools);
2596
- }
2729
+ this._tools = await listAllTools((params) => this.request("tools/list", params)) ?? [];
2597
2730
  this._toolsCache = this._tools;
2598
2731
  this.state = "connected";
2599
2732
  }
@@ -2843,8 +2976,7 @@ var MCPClient = class _MCPClient {
2843
2976
  this.child = void 0;
2844
2977
  }
2845
2978
  this.failPending(`MCP "${this.opts.name}" closed`);
2846
- this.sseTransport?.close();
2847
- this.httpTransport?.close();
2979
+ await Promise.allSettled([this.sseTransport?.close(), this.httpTransport?.close()]);
2848
2980
  this.state = "disconnected";
2849
2981
  }
2850
2982
  request(method, params, timeoutMs = this.opts.requestTimeoutMs ?? 6e4, opts) {
@@ -3085,11 +3217,13 @@ var MCPClient = class _MCPClient {
3085
3217
  }
3086
3218
  }
3087
3219
  handleServerRequest(request3) {
3088
- const message = request3.method === "sampling/createMessage" ? "Client sampling is disabled by policy" : `Method not found: ${request3.method}`;
3089
- const response = {
3220
+ const response = request3.method === "ping" ? { jsonrpc: "2.0", id: request3.id, result: {} } : {
3090
3221
  jsonrpc: "2.0",
3091
3222
  id: request3.id,
3092
- error: { code: -32601, message }
3223
+ error: {
3224
+ code: -32601,
3225
+ message: request3.method === "sampling/createMessage" ? "Client sampling is disabled by policy" : `Method not found: ${request3.method}`
3226
+ }
3093
3227
  };
3094
3228
  try {
3095
3229
  this.child?.stdin?.write(`${JSON.stringify(response)}
@@ -3105,10 +3239,8 @@ var MCPClient = class _MCPClient {
3105
3239
  */
3106
3240
  async handleToolsListChanged() {
3107
3241
  try {
3108
- const toolsRes = await this.request("tools/list", {});
3109
- const tools = normalizeMCPTools(
3110
- toolsRes.result?.tools
3111
- );
3242
+ const tools = await listAllTools((params) => this.request("tools/list", params));
3243
+ if (!tools) return;
3112
3244
  this._tools = tools;
3113
3245
  this._toolsCache = tools;
3114
3246
  for (const listener of this.toolsChangedListeners) {
@@ -3446,7 +3578,7 @@ async function updateMcp(input, deps) {
3446
3578
  servers[input.name] = cfg;
3447
3579
  await persist(deps.configPath, full, servers);
3448
3580
  if (cfg.enabled !== false) {
3449
- return startServer(input.name, cfg, deps, `Server "${input.name}" updated`, { restart: true });
3581
+ return startServer(input.name, cfg, deps, `Server "${input.name}" updated`);
3450
3582
  }
3451
3583
  await safeStop(input.name, deps);
3452
3584
  trackDisabled(deps.registry, cfg);
@@ -3483,7 +3615,7 @@ async function enableMcp(name, deps) {
3483
3615
  cfg.enabled = true;
3484
3616
  servers[name] = cfg;
3485
3617
  await persist(deps.configPath, full, servers);
3486
- return startServer(name, cfg, deps, `Server "${name}" enabled`, { restart: true });
3618
+ return startServer(name, cfg, deps, `Server "${name}" enabled`);
3487
3619
  }
3488
3620
  async function disableMcp(name, deps) {
3489
3621
  if (!name) return { ok: false, message: "Server name is required" };
@@ -3526,12 +3658,19 @@ async function restartMcp(name, deps) {
3526
3658
  }
3527
3659
  const cfg = servers[name];
3528
3660
  if (!cfg) return { ok: false, message: `Server "${name}" is not in config.` };
3529
- return startServer(name, { ...cfg, name }, deps, `Server "${name}" started`, { restart: true });
3661
+ return startServer(name, { ...cfg, name }, deps, `Server "${name}" started`);
3530
3662
  }
3531
3663
  async function discoverMcp(name, deps) {
3532
3664
  if (!name) return { ok: false, message: "Server name is required" };
3533
3665
  const result = await restartMcp(name, deps);
3534
3666
  if (!result.ok) return result;
3667
+ if (liveState(name, deps.registry).state === "dormant") {
3668
+ try {
3669
+ await deps.registry.ensureConnected(name);
3670
+ } catch (err) {
3671
+ return { ok: false, message: `Failed to discover "${name}": ${errMessage(err)}` };
3672
+ }
3673
+ }
3535
3674
  const { state, tools } = liveState(name, deps.registry);
3536
3675
  return {
3537
3676
  ok: true,
@@ -3540,15 +3679,13 @@ async function discoverMcp(name, deps) {
3540
3679
  tools
3541
3680
  };
3542
3681
  }
3543
- async function startServer(name, cfg, deps, okMessage, opts) {
3682
+ async function startServer(name, cfg, deps, okMessage) {
3544
3683
  try {
3545
3684
  const alreadyRegistered = deps.registry.list().some((s) => s.name === name);
3546
- if (alreadyRegistered && opts?.restart) {
3547
- await deps.registry.restart(name);
3548
- } else if (alreadyRegistered) {
3549
- await deps.registry.restart(name);
3685
+ if (alreadyRegistered) {
3686
+ await deps.registry.restart(name, { ...cfg, name, enabled: true });
3550
3687
  } else {
3551
- await deps.registry.start({ ...cfg, enabled: true });
3688
+ await deps.registry.start({ ...cfg, name, enabled: true });
3552
3689
  }
3553
3690
  const { state, tools } = liveState(name, deps.registry);
3554
3691
  return {
@@ -3905,14 +4042,37 @@ function wrapMCPTool(serverName, mcpTool, client, permission = "confirm", observ
3905
4042
  }
3906
4043
  };
3907
4044
  }
4045
+ function renderContentBlock(item) {
4046
+ const type = item["type"];
4047
+ if (type === "text") return typeof item["text"] === "string" ? item["text"] : "";
4048
+ if (type === "image" || type === "audio") {
4049
+ const mime = typeof item["mimeType"] === "string" ? item["mimeType"] : "unknown type";
4050
+ const data = typeof item["data"] === "string" ? item["data"] : "";
4051
+ const kb = Math.max(1, Math.round(data.length * 3 / 4 / 1024));
4052
+ return `[${type} content: ${mime}, ~${kb} KB \u2014 binary payload not inlined]`;
4053
+ }
4054
+ if (type === "resource" && item["resource"] && typeof item["resource"] === "object") {
4055
+ const resource = item["resource"];
4056
+ const uri = typeof resource["uri"] === "string" ? resource["uri"] : "unknown";
4057
+ if (typeof resource["text"] === "string") return `[resource ${uri}]
4058
+ ${resource["text"]}`;
4059
+ if (typeof resource["blob"] === "string") {
4060
+ const mime = typeof resource["mimeType"] === "string" ? `${resource["mimeType"]}, ` : "";
4061
+ return `[resource ${uri}: ${mime}binary payload not inlined]`;
4062
+ }
4063
+ return JSON.stringify(item);
4064
+ }
4065
+ if (type === "resource_link" && typeof item["uri"] === "string") {
4066
+ return `[resource link: ${item["uri"]}]`;
4067
+ }
4068
+ return JSON.stringify(item);
4069
+ }
3908
4070
  function stringify(c) {
3909
4071
  if (typeof c === "string") return c;
3910
4072
  if (Array.isArray(c)) {
3911
4073
  return c.map((item) => {
3912
4074
  if (item && typeof item === "object") {
3913
- const t = item.type;
3914
- if (t === "text") return item.text ?? "";
3915
- return JSON.stringify(item);
4075
+ return renderContentBlock(item);
3916
4076
  }
3917
4077
  return String(item);
3918
4078
  }).join("\n");
@@ -3928,9 +4088,14 @@ function stringify(c) {
3928
4088
 
3929
4089
  // src/registry-connect-loop.ts
3930
4090
  function applySlotTools(ctx, slot, tools, client) {
3931
- if (slot.lazy && slot.registeredLazy && !ctx.lazyMode) return;
4091
+ slot.discoveredTools = tools;
3932
4092
  const allowed = slot.cfg.allowedTools;
3933
4093
  const filtered = tools.filter((t) => !allowed || allowed.includes(t.name));
4094
+ const signature = JSON.stringify(
4095
+ filtered.map((t) => [t.name, t.description ?? null, t.inputSchema ?? null])
4096
+ );
4097
+ const lazyWrappersCurrent = slot.lazy && slot.toolSignature === signature && slot.lazyTools.length === filtered.length && (slot.registeredLazy || ctx.lazyMode);
4098
+ if (lazyWrappersCurrent) return;
3934
4099
  const clientArg = slot.lazy ? () => ctx.ensureConnected(slot.cfg.name) : expectDefined(client);
3935
4100
  const wrapped = filtered.map(
3936
4101
  (t) => wrapMCPTool(slot.cfg.name, t, clientArg, slot.cfg.permission ?? "confirm", {
@@ -3956,9 +4121,16 @@ function applySlotTools(ctx, slot, tools, client) {
3956
4121
  })
3957
4122
  );
3958
4123
  slot.lazyTools = wrapped;
3959
- if (ctx.lazyMode) {
3960
- return;
4124
+ slot.toolSignature = signature;
4125
+ const wasActive = slot.toolNames.length > 0;
4126
+ if (ctx.lazyMode && !wasActive) return;
4127
+ for (const name of slot.toolNames) {
4128
+ try {
4129
+ ctx.toolRegistry.unregister(name);
4130
+ } catch {
4131
+ }
3961
4132
  }
4133
+ slot.toolNames = [];
3962
4134
  for (const tool of wrapped) {
3963
4135
  try {
3964
4136
  ctx.toolRegistry.register(tool, `mcp:${slot.cfg.name}`);
@@ -3967,7 +4139,7 @@ function applySlotTools(ctx, slot, tools, client) {
3967
4139
  ctx.log.warn(`MCP tool "${tool.name}" not registered`, err);
3968
4140
  }
3969
4141
  }
3970
- if (slot.lazy && wrapped.length > 0) slot.registeredLazy = true;
4142
+ if (slot.lazy && !ctx.lazyMode && wrapped.length > 0) slot.registeredLazy = true;
3971
4143
  }
3972
4144
  async function discoverSlotCapabilities(ctx, slot, client) {
3973
4145
  const startedAt = Date.now();
@@ -4019,15 +4191,17 @@ async function discoverSlotCapabilities(ctx, slot, client) {
4019
4191
  async function persistSlotCapabilityManifest(cacheDir, slot) {
4020
4192
  if (!slot.lazy || !cacheDir) return;
4021
4193
  const previous = slot.manifestWrite ?? Promise.resolve();
4022
- const pending = previous.then(
4023
- () => writeCapabilityManifest(cacheDir, slot.cfg.name, manifestConfigHash(slot.cfg), {
4024
- tools: slot.client?.listTools() ?? [],
4194
+ const pending = previous.then(() => {
4195
+ const tools = slot.discoveredTools ?? slot.client?.listTools();
4196
+ if (!tools) return;
4197
+ return writeCapabilityManifest(cacheDir, slot.cfg.name, manifestConfigHash(slot.cfg), {
4198
+ tools,
4025
4199
  serverMetadata: slot.serverMetadata,
4026
4200
  resources: slot.resources,
4027
4201
  resourceTemplates: slot.resourceTemplates,
4028
4202
  prompts: slot.prompts
4029
- })
4030
- );
4203
+ });
4204
+ });
4031
4205
  slot.manifestWrite = pending;
4032
4206
  await pending;
4033
4207
  if (slot.manifestWrite === pending) slot.manifestWrite = void 0;
@@ -4036,7 +4210,7 @@ async function attemptConnectSlot(ctx, slot) {
4036
4210
  const MAX_ATTEMPTS = MCP_CONSTANTS.RECONNECT.MAX_ATTEMPTS;
4037
4211
  let attempt = 0;
4038
4212
  while (attempt < MAX_ATTEMPTS) {
4039
- if (ctx.servers.has(slot.cfg.name) && ctx.servers.get(slot.cfg.name) !== slot) {
4213
+ if (ctx.servers.get(slot.cfg.name) !== slot) {
4040
4214
  return;
4041
4215
  }
4042
4216
  attempt++;
@@ -4056,6 +4230,7 @@ async function attemptConnectSlot(ctx, slot) {
4056
4230
  headers: slot.cfg.headers,
4057
4231
  startupTimeoutMs: slot.cfg.startupTimeoutMs,
4058
4232
  requestTimeoutMs: slot.cfg.requestTimeoutMs,
4233
+ cwd: ctx.cwd,
4059
4234
  allowPrivateNetworks: slot.cfg.allowPrivateNetworks,
4060
4235
  passthroughEnv: slot.cfg.passthroughEnv,
4061
4236
  authorizationProvider: ctx.authorizationProviderFactory?.(slot.cfg)
@@ -4095,6 +4270,7 @@ async function attemptConnectSlot(ctx, slot) {
4095
4270
  slot.reconnectCycles = 0;
4096
4271
  const mc = client;
4097
4272
  const discovered = mc.listTools();
4273
+ slot.discoveredTools = discovered;
4098
4274
  await discoverSlotCapabilities(ctx, slot, mc);
4099
4275
  await persistSlotCapabilityManifest(ctx.cacheDir, slot);
4100
4276
  applySlotTools(ctx, slot, discovered, mc);
@@ -4150,7 +4326,7 @@ async function attemptConnectSlot(ctx, slot) {
4150
4326
  }
4151
4327
  const delay = 500 * 2 ** attempt;
4152
4328
  await new Promise((r) => setTimeout(r, delay));
4153
- if (slot.state === "disconnected" || ctx.servers.has(slot.cfg.name) && ctx.servers.get(slot.cfg.name) !== slot) {
4329
+ if (slot.state === "disconnected" || ctx.servers.get(slot.cfg.name) !== slot) {
4154
4330
  return;
4155
4331
  }
4156
4332
  }
@@ -4167,6 +4343,7 @@ function resetDisconnectedSlotTools(slot, toolRegistry) {
4167
4343
  }
4168
4344
  slot.toolNames = [];
4169
4345
  slot.lazyTools = [];
4346
+ slot.toolSignature = void 0;
4170
4347
  slot.serverMetadata = void 0;
4171
4348
  slot.resources = void 0;
4172
4349
  slot.resourceTemplates = void 0;
@@ -4387,6 +4564,7 @@ var MCPRegistry = class _MCPRegistry {
4387
4564
  log;
4388
4565
  lazyMode;
4389
4566
  cacheDir;
4567
+ cwd;
4390
4568
  idleTimeoutMs;
4391
4569
  authorizationProviderFactory;
4392
4570
  authorizationManager;
@@ -4399,6 +4577,7 @@ var MCPRegistry = class _MCPRegistry {
4399
4577
  this.log = opts.log;
4400
4578
  this.lazyMode = opts.lazyMode ?? false;
4401
4579
  this.cacheDir = opts.cacheDir;
4580
+ this.cwd = opts.cwd;
4402
4581
  this.idleTimeoutMs = opts.idleTimeoutMs ?? MCP_CONSTANTS.IDLE.DEFAULT_TIMEOUT_MS;
4403
4582
  this.authorizationProviderFactory = opts.authorizationProviderFactory;
4404
4583
  this.authorizationManager = opts.authorizationManager;
@@ -4558,7 +4737,6 @@ var MCPRegistry = class _MCPRegistry {
4558
4737
  }
4559
4738
  }
4560
4739
  this.log.info(`MCP server "${name}" activated (${slot.toolNames.length} tools)`);
4561
- this.events.emit("mcp.server.connected", { name, toolCount: slot.toolNames.length });
4562
4740
  }
4563
4741
  /**
4564
4742
  * Unregister all tools for a given server from the tool registry.
@@ -4578,9 +4756,27 @@ var MCPRegistry = class _MCPRegistry {
4578
4756
  }
4579
4757
  slot.toolNames = [];
4580
4758
  this.log.info(`MCP server "${name}" deactivated (${count} tools removed)`);
4581
- this.events.emit("mcp.server.disconnected", { name, reason: "deactivate" });
4582
4759
  return count;
4583
4760
  }
4761
+ /**
4762
+ * The tools a server offers — bare names, descriptions and input schemas —
4763
+ * without activating, registering or waking it. In token-saving mode the
4764
+ * model reaches MCP only through `mcp_use`, which needs the bare tool name
4765
+ * and its input shape; before this there was no way to learn either short
4766
+ * of guessing and reading the error. Honors `allowedTools`. Returns
4767
+ * `undefined` for an unknown server, `[]` when nothing was discovered yet.
4768
+ */
4769
+ describeTools(name) {
4770
+ const slot = this.servers.get(name);
4771
+ if (!slot) return void 0;
4772
+ const allowed = slot.cfg.allowedTools;
4773
+ const tools = slot.discoveredTools ?? slot.client?.listTools() ?? [];
4774
+ return tools.filter((tool) => !allowed || allowed.includes(tool.name)).map((tool) => ({
4775
+ name: tool.name,
4776
+ ...tool.description !== void 0 ? { description: tool.description } : {},
4777
+ inputSchema: structuredClone(tool.inputSchema)
4778
+ }));
4779
+ }
4584
4780
  /**
4585
4781
  * Check whether a server's tools are currently registered.
4586
4782
  */
@@ -4617,12 +4813,52 @@ var MCPRegistry = class _MCPRegistry {
4617
4813
  this.recordOperation(slot, "stop", "manual");
4618
4814
  this.events.emit("mcp.server.disconnected", { name, reason: "stop" });
4619
4815
  }
4620
- async restart(name) {
4816
+ /**
4817
+ * Stop and start a registered server. Pass `nextCfg` to apply an edited
4818
+ * configuration: without it the slot reconnects with the config it was
4819
+ * started with, so an update/enable routed through restart() silently kept
4820
+ * the old command, url, env, permission and lazy flag until the next boot.
4821
+ */
4822
+ /**
4823
+ * Put a running server to sleep while keeping its configuration enabled.
4824
+ *
4825
+ * A lazy server goes `dormant`: the process stops but its tools stay
4826
+ * registered and the next call wakes it. Surfaces previously used stop() for
4827
+ * this, which unregistered a lazy server's tools — "sleep" silently became
4828
+ * "unreachable until restarted". Eager servers have no dormant state, so
4829
+ * for them sleep is a stop.
4830
+ */
4831
+ async sleep(name) {
4832
+ const slot = this.requireSlot(name);
4833
+ if (!slot.lazy) {
4834
+ await this.stop(name);
4835
+ return;
4836
+ }
4837
+ if (slot.state === "dormant") return;
4838
+ if (slot.operations.inFlightCalls > 0) {
4839
+ throw new Error(`MCP server "${name}" has tool calls in flight \u2014 try again when they finish`);
4840
+ }
4841
+ await sleepIdleSlot(this.idleContext(), slot);
4842
+ }
4843
+ async restart(name, nextCfg) {
4621
4844
  const slot = this.servers.get(name);
4622
4845
  if (!slot) throw new Error(`MCP server "${name}" not registered`);
4846
+ if (nextCfg && nextCfg.name !== name) {
4847
+ throw new Error(`MCP restart config names "${nextCfg.name}", expected "${name}"`);
4848
+ }
4849
+ if (nextCfg?.enabled === false) {
4850
+ await this.stop(name);
4851
+ this.markDisabled(nextCfg);
4852
+ return;
4853
+ }
4623
4854
  slot.operations.restartCount++;
4624
4855
  this.recordOperation(slot, "restart", "manual");
4625
4856
  await this.stop(name);
4857
+ if (nextCfg) {
4858
+ slot.cfg = nextCfg;
4859
+ slot.lazy = !!nextCfg.lazy && !!this.cacheDir;
4860
+ slot.discoveredTools = void 0;
4861
+ }
4626
4862
  slot.attempts = 0;
4627
4863
  slot.reconnectCycles = 0;
4628
4864
  if (slot.lazy) {
@@ -4744,6 +4980,7 @@ var MCPRegistry = class _MCPRegistry {
4744
4980
  log: this.log,
4745
4981
  lazyMode: this.lazyMode,
4746
4982
  cacheDir: this.cacheDir,
4983
+ cwd: this.cwd,
4747
4984
  authorizationProviderFactory: this.authorizationProviderFactory,
4748
4985
  operationListeners: this.operationListeners,
4749
4986
  ensureConnected: (name) => this.ensureConnected(name),
@@ -4855,21 +5092,9 @@ var MCPRegistry = class _MCPRegistry {
4855
5092
  onToolsChanged = (name, _tools) => {
4856
5093
  const slot = this.servers.get(name);
4857
5094
  if (!slot?.client) return;
4858
- for (const t of slot.toolNames) {
4859
- try {
4860
- this.toolRegistry.unregister(t);
4861
- } catch {
4862
- }
4863
- }
4864
- slot.toolNames = [];
4865
- slot.registeredLazy = false;
4866
5095
  const discovered = slot.client.listTools();
4867
5096
  this.applyTools(slot, discovered, slot.client);
4868
5097
  void this.persistCapabilityManifest(slot);
4869
- this.events.emit("mcp.server.connected", {
4870
- name: slot.cfg.name,
4871
- toolCount: slot.toolNames.length
4872
- });
4873
5098
  this.log.info(
4874
5099
  `MCP server "${slot.cfg.name}" tools refreshed (${this.toolNamesForSlot(slot).length} active)`
4875
5100
  );
@@ -4899,7 +5124,7 @@ var MCPRegistry = class _MCPRegistry {
4899
5124
  }
4900
5125
  onChildExit = (name, code, _signal) => {
4901
5126
  const slot = this.servers.get(name);
4902
- if (!slot) return;
5127
+ if (slot?.state !== "connected") return;
4903
5128
  if (slot.lazy) {
4904
5129
  this.recordFailure(slot, "transport", "process-exit-lazy");
4905
5130
  markLazySlotDormant(slot, this.events, `exit:${code ?? "unknown"}`, {
@@ -4918,7 +5143,7 @@ var MCPRegistry = class _MCPRegistry {
4918
5143
  /** Handles SSE / streamable-http disconnect — same recovery as stdio child exit. */
4919
5144
  onTransportDisconnect = (name) => {
4920
5145
  const slot = this.servers.get(name);
4921
- if (!slot) return;
5146
+ if (slot?.state !== "connected") return;
4922
5147
  if (slot.lazy) {
4923
5148
  this.recordFailure(slot, "transport", "http-disconnect-lazy");
4924
5149
  markLazySlotDormant(slot, this.events, "http-disconnect", {
@@ -4954,7 +5179,7 @@ var MCPRegistry = class _MCPRegistry {
4954
5179
  slot.reconnectCycles++;
4955
5180
  slot.operations.reconnectCount++;
4956
5181
  this.recordOperation(slot, "reconnect", "automatic");
4957
- await this.attemptConnect(slot);
5182
+ await this.singleFlightConnect(slot);
4958
5183
  }
4959
5184
  recordSuccess(slot, resetFailures = true) {
4960
5185
  recordRegistrySuccess(slot, resetFailures);
@@ -5639,6 +5864,8 @@ var MCPRefreshingAuthorizationProvider = class {
5639
5864
  }
5640
5865
  options;
5641
5866
  refreshPromise;
5867
+ /** Refresh token the authorization server rejected; cleared by a new authorization. */
5868
+ rejectedRefreshToken;
5642
5869
  resource;
5643
5870
  refreshSkewMs;
5644
5871
  async getAccessToken(context) {
@@ -5676,16 +5903,28 @@ var MCPRefreshingAuthorizationProvider = class {
5676
5903
  }
5677
5904
  async refreshInner(state) {
5678
5905
  const refreshToken = state.tokenSet.refreshToken;
5679
- if (!refreshToken) {
5680
- this.emit("reauth_required", state);
5906
+ if (!refreshToken || refreshToken === this.rejectedRefreshToken) {
5907
+ if (!refreshToken) this.emit("reauth_required", state);
5681
5908
  return void 0;
5682
5909
  }
5683
- const tokenSet = await refreshMcpAccessToken({
5684
- authorizationServer: state.authorizationServer,
5685
- clientId: state.clientId,
5686
- resource: state.resource,
5687
- refreshToken
5688
- });
5910
+ let tokenSet;
5911
+ try {
5912
+ tokenSet = await refreshMcpAccessToken({
5913
+ authorizationServer: state.authorizationServer,
5914
+ clientId: state.clientId,
5915
+ resource: state.resource,
5916
+ refreshToken
5917
+ });
5918
+ } catch (err) {
5919
+ const current = await this.options.store.load(this.options.serverName, this.resource).catch(() => void 0);
5920
+ if (current && current.tokenSet.refreshToken !== refreshToken) return current;
5921
+ if (err instanceof MCPOAuthHttpError && err.status >= 400 && err.status < 500) {
5922
+ this.rejectedRefreshToken = refreshToken;
5923
+ this.emit("reauth_required", state);
5924
+ return void 0;
5925
+ }
5926
+ throw err;
5927
+ }
5689
5928
  const next = normalizeStoredAuthorization({
5690
5929
  ...state,
5691
5930
  tokenSet,