@corenel/sidecar 0.8.2 → 0.8.3

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.
Files changed (2) hide show
  1. package/dist/cli.js +281 -47
  2. package/package.json +5 -5
package/dist/cli.js CHANGED
@@ -14239,10 +14239,10 @@ var require_filters = __commonJS({
14239
14239
  _exports.striptags = striptags;
14240
14240
  function title(str10) {
14241
14241
  str10 = normalize3(str10, "");
14242
- var words = str10.split(" ").map(function(word) {
14242
+ var words2 = str10.split(" ").map(function(word) {
14243
14243
  return capitalize(word);
14244
14244
  });
14245
- return r2.copySafeness(str10, words.join(" "));
14245
+ return r2.copySafeness(str10, words2.join(" "));
14246
14246
  }
14247
14247
  _exports.title = title;
14248
14248
  function trim(str10) {
@@ -14297,7 +14297,7 @@ var require_filters = __commonJS({
14297
14297
  length = Infinity;
14298
14298
  }
14299
14299
  var noFollowAttr = nofollow === true ? ' rel="nofollow"' : "";
14300
- var words = str10.split(/(\s+)/).filter(function(word) {
14300
+ var words2 = str10.split(/(\s+)/).filter(function(word) {
14301
14301
  return word && word.length;
14302
14302
  }).map(function(word) {
14303
14303
  var matches = word.match(puncRe);
@@ -14317,13 +14317,13 @@ var require_filters = __commonJS({
14317
14317
  }
14318
14318
  return word;
14319
14319
  });
14320
- return words.join("");
14320
+ return words2.join("");
14321
14321
  }
14322
14322
  _exports.urlize = urlize;
14323
14323
  function wordcount(str10) {
14324
14324
  str10 = normalize3(str10, "");
14325
- var words = str10 ? str10.match(/\w+/g) : null;
14326
- return words ? words.length : null;
14325
+ var words2 = str10 ? str10.match(/\w+/g) : null;
14326
+ return words2 ? words2.length : null;
14327
14327
  }
14328
14328
  _exports.wordcount = wordcount;
14329
14329
  function float(val, def) {
@@ -17408,10 +17408,10 @@ ${yamlContent}` : yamlContent;
17408
17408
  if (!str10 || typeof str10 !== "string") {
17409
17409
  return "";
17410
17410
  }
17411
- const words = str10.split(/\s+/);
17411
+ const words2 = str10.split(/\s+/);
17412
17412
  const lines = [];
17413
17413
  let currentLine = "";
17414
- for (const word of words) {
17414
+ for (const word of words2) {
17415
17415
  if (currentLine.length + word.length + 1 <= width) {
17416
17416
  currentLine += (currentLine ? " " : "") + word;
17417
17417
  } else {
@@ -18083,10 +18083,10 @@ ${content}` : parentData.content;
18083
18083
  if (!str10 || typeof str10 !== "string") {
18084
18084
  return "";
18085
18085
  }
18086
- const words = str10.split(/\s+/);
18086
+ const words2 = str10.split(/\s+/);
18087
18087
  const lines = [];
18088
18088
  let currentLine = "";
18089
- for (const word of words) {
18089
+ for (const word of words2) {
18090
18090
  if (currentLine.length + word.length + 1 <= width) {
18091
18091
  currentLine += (currentLine ? " " : "") + word;
18092
18092
  } else {
@@ -24888,7 +24888,7 @@ var require_abort_controller = __commonJS({
24888
24888
  "use strict";
24889
24889
  Object.defineProperty(exports, "__esModule", { value: true });
24890
24890
  var eventTargetShim = require_event_target_shim();
24891
- var AbortSignal = class extends eventTargetShim.EventTarget {
24891
+ var AbortSignal2 = class extends eventTargetShim.EventTarget {
24892
24892
  /**
24893
24893
  * AbortSignal cannot be constructed directly.
24894
24894
  */
@@ -24907,9 +24907,9 @@ var require_abort_controller = __commonJS({
24907
24907
  return aborted;
24908
24908
  }
24909
24909
  };
24910
- eventTargetShim.defineEventAttribute(AbortSignal.prototype, "abort");
24910
+ eventTargetShim.defineEventAttribute(AbortSignal2.prototype, "abort");
24911
24911
  function createAbortSignal() {
24912
- const signal = Object.create(AbortSignal.prototype);
24912
+ const signal = Object.create(AbortSignal2.prototype);
24913
24913
  eventTargetShim.EventTarget.call(signal);
24914
24914
  abortedFlags.set(signal, false);
24915
24915
  return signal;
@@ -24922,11 +24922,11 @@ var require_abort_controller = __commonJS({
24922
24922
  signal.dispatchEvent({ type: "abort" });
24923
24923
  }
24924
24924
  var abortedFlags = /* @__PURE__ */ new WeakMap();
24925
- Object.defineProperties(AbortSignal.prototype, {
24925
+ Object.defineProperties(AbortSignal2.prototype, {
24926
24926
  aborted: { enumerable: true }
24927
24927
  });
24928
24928
  if (typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol") {
24929
- Object.defineProperty(AbortSignal.prototype, Symbol.toStringTag, {
24929
+ Object.defineProperty(AbortSignal2.prototype, Symbol.toStringTag, {
24930
24930
  configurable: true,
24931
24931
  value: "AbortSignal"
24932
24932
  });
@@ -24970,11 +24970,11 @@ var require_abort_controller = __commonJS({
24970
24970
  });
24971
24971
  }
24972
24972
  exports.AbortController = AbortController2;
24973
- exports.AbortSignal = AbortSignal;
24973
+ exports.AbortSignal = AbortSignal2;
24974
24974
  exports.default = AbortController2;
24975
24975
  module.exports = AbortController2;
24976
24976
  module.exports.AbortController = module.exports["default"] = AbortController2;
24977
- module.exports.AbortSignal = AbortSignal;
24977
+ module.exports.AbortSignal = AbortSignal2;
24978
24978
  }
24979
24979
  });
24980
24980
 
@@ -41720,6 +41720,9 @@ var BUILTIN_NAMESPACES = {
41720
41720
  spawn_agents: "agent",
41721
41721
  call_agent: "agent",
41722
41722
  todo_write: "agent",
41723
+ // Finds deferred MCP tools (toolSearch.ts). Deliberately not `system`, which
41724
+ // the standard policy denies: search is a read and must always be reachable.
41725
+ tool_search: "agent",
41723
41726
  // Named here as a backstop; the sidecar's own decls also carry `namespace`,
41724
41727
  // and an explicit field always wins (resolveNamespace). Both say the same
41725
41728
  // thing so a sidecar that predates this still lands in the right namespace.
@@ -41807,14 +41810,115 @@ ${ref.preview}${ref.bytes > ref.preview.length ? "\n\u2026(truncated)" : ""}` :
41807
41810
  // ../harness/prompts/toolsMarker.ts
41808
41811
  var TOOLS_MARKER = "\uE000PROMPD_TOOLS\uE000";
41809
41812
  function formatToolLines(tools) {
41810
- return tools.map((t2) => `- \`${t2.name}\`: ${t2.description}`).join("\n");
41813
+ return tools.map((t2) => `\`${t2.name}\``).join(", ");
41811
41814
  }
41812
41815
  function fillToolsMarker(system, tools) {
41813
41816
  if (!system.includes(TOOLS_MARKER)) return system;
41814
41817
  return system.replaceAll(TOOLS_MARKER, tools.length ? formatToolLines(tools) : "(no tools available this run)");
41815
41818
  }
41816
41819
 
41820
+ // ../harness/tools/toolSearch.ts
41821
+ init_protocol();
41822
+ var TOOL_SEARCH_NAME = "tool_search";
41823
+ var LOADED_TOOLS_CAP = 24;
41824
+ var DEFAULT_LIMIT = 8;
41825
+ var MAX_LIMIT = 20;
41826
+ function serverOf(tool) {
41827
+ return parseMcpWireName(tool.name)?.server ?? "mcp";
41828
+ }
41829
+ function words(s2) {
41830
+ return s2.toLowerCase().split(/[^a-z0-9]+/).filter((w2) => w2.length > 1);
41831
+ }
41832
+ function rankTools(query, tools, opts = {}) {
41833
+ const limit2 = Math.min(MAX_LIMIT, Math.max(1, Math.floor(opts.limit ?? DEFAULT_LIMIT)));
41834
+ const pool = opts.server ? tools.filter((t2) => serverOf(t2) === opts.server) : tools;
41835
+ const q2 = words(query);
41836
+ const scored = pool.map((tool, i2) => {
41837
+ const parsed = parseMcpWireName(tool.name);
41838
+ const nameWords = words(parsed?.tool ?? tool.name);
41839
+ const serverWords = words(parsed?.server ?? "");
41840
+ const descWords = words(tool.description ?? "");
41841
+ let score = 0;
41842
+ for (const w2 of q2) {
41843
+ if (nameWords.includes(w2)) score += 3;
41844
+ else if (nameWords.some((n2) => n2.startsWith(w2) || w2.startsWith(n2))) score += 2;
41845
+ if (serverWords.includes(w2)) score += 2;
41846
+ if (descWords.includes(w2)) score += 1;
41847
+ else if (descWords.some((d2) => d2.startsWith(w2))) score += 0.5;
41848
+ }
41849
+ return { tool, score, i: i2 };
41850
+ });
41851
+ return scored.filter((s2) => s2.score > 0 || opts.server).sort((a2, b2) => b2.score - a2.score || a2.i - b2.i).slice(0, limit2).map((s2) => s2.tool);
41852
+ }
41853
+ function serverIndex(deferred) {
41854
+ const counts = /* @__PURE__ */ new Map();
41855
+ for (const t2 of deferred) counts.set(serverOf(t2), (counts.get(serverOf(t2)) ?? 0) + 1);
41856
+ const lines = [...counts].map(([s2, n2]) => `- ${s2} \xB7 ${n2} tool${n2 === 1 ? "" : "s"}`);
41857
+ return [
41858
+ `Connected services. Their tools are not loaded until you find them: call \`${TOOL_SEARCH_NAME}\` with what you want to do (and optionally the server) before concluding something is unavailable. Tools it returns can be called directly from then on.`,
41859
+ ...lines
41860
+ ].join("\n");
41861
+ }
41862
+ function createToolLoader(opts) {
41863
+ const cap = opts.cap ?? LOADED_TOOLS_CAP;
41864
+ const byName = new Map(opts.deferred.map((t2) => [t2.name, t2]));
41865
+ const incoming = opts.loaded ?? [];
41866
+ let order = incoming.filter((n2, i2) => byName.has(n2) && incoming.indexOf(n2) === i2).slice(-cap);
41867
+ if (order.length !== incoming.length) opts.onChange?.([...order]);
41868
+ const add = (names) => {
41869
+ const before = new Set(order);
41870
+ for (const n2 of names) {
41871
+ order = order.filter((x2) => x2 !== n2);
41872
+ order.push(n2);
41873
+ }
41874
+ if (order.length > cap) order = order.slice(order.length - cap);
41875
+ const changed = order.length !== before.size || order.some((n2) => !before.has(n2));
41876
+ if (changed) opts.onChange?.([...order]);
41877
+ };
41878
+ const searchTool = {
41879
+ name: TOOL_SEARCH_NAME,
41880
+ namespace: "agent",
41881
+ description: "Find tools on connected services (MCP servers) by what you want to do. Returns matching tools with their input schemas and makes them callable from your next step. Search before concluding a service cannot do something.",
41882
+ parameters: {
41883
+ type: "object",
41884
+ properties: {
41885
+ query: { type: "string", description: 'What you want to do, in plain words, e.g. "send an email" or "list calendar events".' },
41886
+ server: { type: "string", description: "Only search this server (a name from the connected services list)." },
41887
+ limit: { type: "number", description: `Most results to return (default ${DEFAULT_LIMIT}, max ${MAX_LIMIT}).` }
41888
+ },
41889
+ required: ["query"]
41890
+ },
41891
+ mutates: false,
41892
+ // The schemas ARE the answer; offloading them behind a pointer would hide
41893
+ // exactly what the model searched for.
41894
+ noOffload: true,
41895
+ async run(args) {
41896
+ const query = typeof args.query === "string" ? args.query.trim() : "";
41897
+ const server = typeof args.server === "string" && args.server ? args.server : void 0;
41898
+ if (!query && !server) return "Error: pass a query describing what you want to do.";
41899
+ const limit2 = typeof args.limit === "number" ? args.limit : void 0;
41900
+ const found = rankTools(query, opts.deferred, { server, limit: limit2 });
41901
+ add(found.map((t2) => t2.name));
41902
+ const servers = [...new Set(opts.deferred.map(serverOf))];
41903
+ return JSON.stringify({
41904
+ tools: found.map((t2) => ({ name: t2.name, server: serverOf(t2), description: t2.description, inputSchema: t2.parameters })),
41905
+ note: found.length ? "These tools are now loaded; call them directly." : `No tools matched. Connected servers: ${servers.join(", ") || "(none)"}. Try other words or name a server.`
41906
+ });
41907
+ }
41908
+ };
41909
+ return {
41910
+ searchTool,
41911
+ loaded: () => order.map((n2) => byName.get(n2)).filter((t2) => !!t2),
41912
+ loadedNames: () => [...order],
41913
+ touch: (name) => {
41914
+ if (!order.includes(name)) return;
41915
+ order = [...order.filter((n2) => n2 !== name), name];
41916
+ }
41917
+ };
41918
+ }
41919
+
41817
41920
  // ../harness/core/loop.ts
41921
+ var MAX_PROVIDER_TOOLS = 128;
41818
41922
  async function runAgent(args) {
41819
41923
  const { client, system, tools, toolCtx } = args;
41820
41924
  const policy = args.policy ?? STANDARD_POLICY;
@@ -41840,17 +41944,28 @@ async function runAgent(args) {
41840
41944
  turnAc?.abort();
41841
41945
  });
41842
41946
  const messages = [...args.messages];
41843
- const filledSystem = system ? fillToolsMarker(system, tools) : system;
41947
+ const deferred = args.deferredTools ?? [];
41948
+ const loader = deferred.length ? createToolLoader({ deferred, loaded: args.loadedTools, onChange: (names) => emit({ type: "tools-loaded", names }) }) : null;
41949
+ const eager = loader ? [...tools, loader.searchTool] : tools;
41950
+ const capped = (list) => list.length > MAX_PROVIDER_TOOLS ? list.slice(0, MAX_PROVIDER_TOOLS) : list;
41951
+ const activeTools = () => capped(loader ? [...eager, ...loader.loaded()] : eager);
41952
+ let sent = activeTools();
41953
+ const indexedSystem = loader ? system ? `${system}
41954
+
41955
+ ${serverIndex(deferred)}` : serverIndex(deferred) : system;
41956
+ const filledSystem = indexedSystem ? fillToolsMarker(indexedSystem, sent) : indexedSystem;
41844
41957
  if (filledSystem && !messages.some((m2) => m2.role === "system")) {
41845
41958
  messages.unshift({ role: "system", content: filledSystem });
41846
41959
  }
41847
- const toolDefs = tools.map((t2) => ({
41960
+ const defsFor = (list) => list.map((t2) => ({
41848
41961
  type: "function",
41849
41962
  function: { name: t2.name, description: t2.description, parameters: t2.parameters }
41850
41963
  }));
41851
- const toolByName = new Map(tools.map((t2) => [t2.name, t2]));
41964
+ let toolDefs = defsFor(sent);
41965
+ let toolByName = new Map(sent.map((t2) => [t2.name, t2]));
41852
41966
  if (compressionStrategy === "deferred" && compression && compression.mode !== "off" && toolCtx.recall) {
41853
- await compactIncomingOffloads(messages, toolByName, compression, toolCtx.recall, emit);
41967
+ const known = new Map([...eager, ...deferred].map((t2) => [t2.name, t2]));
41968
+ await compactIncomingOffloads(messages, known, compression, toolCtx.recall, emit);
41854
41969
  }
41855
41970
  const usage = { promptTokens: 0, completionTokens: 0 };
41856
41971
  let text = "";
@@ -41879,6 +41994,11 @@ async function runAgent(args) {
41879
41994
  break;
41880
41995
  }
41881
41996
  }
41997
+ if (loader) {
41998
+ sent = activeTools();
41999
+ toolDefs = defsFor(sent);
42000
+ toolByName = new Map(sent.map((t2) => [t2.name, t2]));
42001
+ }
41882
42002
  let streamed;
41883
42003
  let streamFail = null;
41884
42004
  const model = args.getModel?.() ?? args.model;
@@ -42005,6 +42125,7 @@ async function runAgent(args) {
42005
42125
  emit({ type: "tool-result", id: call.id, name: call.name, result: "unknown tool", isError: true });
42006
42126
  continue;
42007
42127
  }
42128
+ loader?.touch(tool.name);
42008
42129
  const address = toolAddress(tool);
42009
42130
  const gate = evaluateToolCall(policy, gstate, { name: call.name, exists: true, address });
42010
42131
  if (!gate.ok) {
@@ -46193,7 +46314,7 @@ var FLAG_TO_KEY = {
46193
46314
  "--default-model": "defaultModel",
46194
46315
  "--name": "name"
46195
46316
  };
46196
- var DEFAULT_GUEST_IMAGE2 = "prompd/corenel-sandbox:0.8.2";
46317
+ var DEFAULT_GUEST_IMAGE2 = "prompd/corenel-sandbox:0.8.3";
46197
46318
  function parseArgsWithProvenance(argv) {
46198
46319
  const explicit = /* @__PURE__ */ new Set();
46199
46320
  for (const a2 of argv) {
@@ -48914,7 +49035,8 @@ var McpHttpClient = class {
48914
49035
  * loop -- a credential that fails twice is refused, not flaky. */
48915
49036
  async send(method, params, isRetry = false) {
48916
49037
  const headers = await this.buildHeaders();
48917
- const res = await this.request(headers, method, params);
49038
+ const id = this.nextRequestId++;
49039
+ const res = await this.request(headers, id, method, params);
48918
49040
  if (res.status === 401) {
48919
49041
  if (!isRetry && await this.auth.onUnauthorized()) {
48920
49042
  return this.send(method, params, true);
@@ -48925,26 +49047,77 @@ var McpHttpClient = class {
48925
49047
  }
48926
49048
  const sessionId = res.headers.get(SESSION_HEADER);
48927
49049
  if (sessionId) this.sessionId = sessionId;
49050
+ if (!res.ok) {
49051
+ throw new Error(`MCP server "${this.server.name}" returned HTTP ${res.status}`);
49052
+ }
48928
49053
  const contentType = res.headers.get("content-type") ?? "";
49054
+ let body2;
48929
49055
  if (contentType.includes("text/event-stream")) {
48930
- throw new Error(
48931
- `MCP server "${this.server.name}" answered with an SSE stream (content-type: ${contentType}); streaming MCP responses are not supported yet`
48932
- );
48933
- }
48934
- if (!contentType.includes("application/json")) {
49056
+ body2 = await this.readSseResponse(res, id);
49057
+ } else if (contentType.includes("application/json")) {
49058
+ body2 = await res.json();
49059
+ } else {
48935
49060
  throw new Error(
48936
49061
  `MCP server "${this.server.name}" answered with an unexpected content-type: "${contentType || "(none)"}"`
48937
49062
  );
48938
49063
  }
48939
- if (!res.ok) {
48940
- throw new Error(`MCP server "${this.server.name}" returned HTTP ${res.status}`);
48941
- }
48942
- const body2 = await res.json();
48943
49064
  if (body2.error) {
48944
49065
  throw new Error(`MCP server "${this.server.name}" returned error ${body2.error.code}: ${body2.error.message}`);
48945
49066
  }
48946
49067
  return body2.result;
48947
49068
  }
49069
+ /** Read an SSE body until the JSON-RPC response for `id` arrives.
49070
+ *
49071
+ * Events are separated by a blank line; an event's `data:` lines are joined
49072
+ * with newlines, per the SSE spec, and parsed as one JSON-RPC message.
49073
+ * Notifications (no `id`) and responses to other ids are skipped. The reader
49074
+ * is cancelled once the answer is in, so a server that keeps the stream open
49075
+ * after replying cannot hold this call open with it. */
49076
+ async readSseResponse(res, id) {
49077
+ if (!res.body) {
49078
+ throw new Error(`MCP server "${this.server.name}" closed the stream without a response`);
49079
+ }
49080
+ const reader = res.body.getReader();
49081
+ const decoder = new TextDecoder();
49082
+ let buffer = "";
49083
+ try {
49084
+ for (; ; ) {
49085
+ const { done, value } = await reader.read();
49086
+ if (value) buffer += decoder.decode(value, { stream: true });
49087
+ buffer = buffer.replace(/\r\n/g, "\n");
49088
+ let boundary = buffer.indexOf("\n\n");
49089
+ while (boundary !== -1) {
49090
+ const event = buffer.slice(0, boundary);
49091
+ buffer = buffer.slice(boundary + 2);
49092
+ const match = this.matchSseEvent(event, id);
49093
+ if (match) return match;
49094
+ boundary = buffer.indexOf("\n\n");
49095
+ }
49096
+ if (done) {
49097
+ const tail3 = buffer.trim() ? this.matchSseEvent(buffer, id) : null;
49098
+ if (tail3) return tail3;
49099
+ throw new Error(`MCP server "${this.server.name}" closed the stream without a response`);
49100
+ }
49101
+ }
49102
+ } finally {
49103
+ reader.cancel().catch(() => {
49104
+ });
49105
+ }
49106
+ }
49107
+ /** The JSON-RPC response in one SSE event, if it answers `id`; else null. */
49108
+ matchSseEvent(event, id) {
49109
+ const data = event.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
49110
+ if (!data) return null;
49111
+ let parsed;
49112
+ try {
49113
+ parsed = JSON.parse(data);
49114
+ } catch {
49115
+ return null;
49116
+ }
49117
+ if (!isRecord3(parsed) || parsed.id !== id) return null;
49118
+ if (!("result" in parsed) && !("error" in parsed)) return null;
49119
+ return parsed;
49120
+ }
48948
49121
  /** The bare `fetch()` call, wrapped -- every OTHER throw in `send` already
48949
49122
  * names the server; this is the one call whose own failures (DNS miss,
48950
49123
  * connection refused, TLS failure) never reach `send`'s response-handling
@@ -48954,12 +49127,12 @@ var McpHttpClient = class {
48954
49127
  * whose entire job is diagnosis. `err.cause` carries the real reason
48955
49128
  * (ECONNREFUSED, ENOTFOUND, a TLS alert); undici puts it there rather than
48956
49129
  * in `err.message`. */
48957
- async request(headers, method, params) {
49130
+ async request(headers, id, method, params) {
48958
49131
  try {
48959
49132
  return await this.fetchImpl(this.server.url, {
48960
49133
  method: "POST",
48961
49134
  headers,
48962
- body: JSON.stringify({ jsonrpc: "2.0", id: this.nextRequestId++, method, params })
49135
+ body: JSON.stringify({ jsonrpc: "2.0", id, method, params })
48963
49136
  });
48964
49137
  } catch (err) {
48965
49138
  const cause = err instanceof Error && err.cause instanceof Error ? err.cause.message : err instanceof Error ? err.message : String(err);
@@ -49020,6 +49193,25 @@ function authProviderFor(stateDir, server, fetchImpl = fetch, accountToken = asy
49020
49193
 
49021
49194
  // src/mcp/registry.ts
49022
49195
  init_store();
49196
+
49197
+ // src/mcp/timedFetch.ts
49198
+ var AUTH_REQUEST_TIMEOUT_MS = 15e3;
49199
+ function timedFetch(ms, inner = fetch) {
49200
+ return async (input, init2 = {}) => {
49201
+ const deadline = AbortSignal.timeout(ms);
49202
+ const signal = init2.signal ? AbortSignal.any([init2.signal, deadline]) : deadline;
49203
+ try {
49204
+ return await inner(input, { ...init2, signal });
49205
+ } catch (err) {
49206
+ if (deadline.aborted && !init2.signal?.aborted) {
49207
+ throw new Error(`no answer within ${ms / 1e3}s`, { cause: err });
49208
+ }
49209
+ throw err;
49210
+ }
49211
+ };
49212
+ }
49213
+
49214
+ // src/mcp/registry.ts
49023
49215
  async function accountTokenFor(stateDir) {
49024
49216
  try {
49025
49217
  const cred = await resolveCredential(process.env, stateDir);
@@ -49029,8 +49221,12 @@ async function accountTokenFor(stateDir) {
49029
49221
  }
49030
49222
  }
49031
49223
  function defaultClientFactory(stateDir, server) {
49032
- return new McpHttpClient(server, authProviderFor(stateDir, server, fetch, () => accountTokenFor(stateDir)));
49224
+ return new McpHttpClient(
49225
+ server,
49226
+ authProviderFor(stateDir, server, timedFetch(AUTH_REQUEST_TIMEOUT_MS), () => accountTokenFor(stateDir))
49227
+ );
49033
49228
  }
49229
+ var SERVER_CONNECT_TIMEOUT_MS = 3e4;
49034
49230
  var McpRegistry = class {
49035
49231
  constructor(stateDir, clientFactory = defaultClientFactory) {
49036
49232
  this.stateDir = stateDir;
@@ -49054,27 +49250,60 @@ var McpRegistry = class {
49054
49250
  * a server that fails to initialize, or fails listing tools, is recorded
49055
49251
  * in `status()` with zero tools and the daemon keeps starting.
49056
49252
  */
49057
- async connectAll() {
49253
+ async connectAll(opts = {}) {
49058
49254
  this.connected.clear();
49059
49255
  this.statuses.clear();
49060
49256
  const servers = await readMcpServers(this.stateDir);
49061
49257
  this.serverOrder = servers.map((s2) => s2.name);
49062
- await Promise.all(servers.map((server) => this.connectOne(server)));
49258
+ const timeoutMs = opts.timeoutMs ?? SERVER_CONNECT_TIMEOUT_MS;
49259
+ await Promise.all(servers.map((server) => this.connectOne(server, timeoutMs, opts.onServer)));
49063
49260
  }
49064
- async connectOne(server) {
49065
- try {
49261
+ /* BOUNDED, AND SETTLED EXACTLY ONCE.
49262
+ *
49263
+ * Every server connects under one Promise.all, and daemon startup awaits it.
49264
+ * With no bound, one stalled server -- measured: an expired Microsoft 365
49265
+ * token refreshing against a cold service, still waiting at 60 seconds --
49266
+ * left the terminal silent after `mirrors sessions`, indistinguishable from a
49267
+ * crash.
49268
+ *
49269
+ * The deadline covers the whole connect (any credential refresh, initialize,
49270
+ * tools/list), not each request, because several individually bounded steps
49271
+ * in a row can still add up to a hang. The attempt that loses the race keeps
49272
+ * running; its late result is deliberately ignored, so a server reported as
49273
+ * timed out cannot quietly become connected afterwards -- status would then
49274
+ * tell doctor one thing and the registered tools another. */
49275
+ async connectOne(server, timeoutMs, onServer) {
49276
+ let timer;
49277
+ const deadline = new Promise((resolve13) => {
49278
+ timer = setTimeout(() => resolve13("timeout"), timeoutMs);
49279
+ });
49280
+ const attempt = (async () => {
49066
49281
  const client = this.clientFactory(this.stateDir, server);
49067
49282
  await client.initialize();
49068
49283
  const tools = await client.listTools();
49069
- this.connected.set(server.name, { client, tools });
49070
- this.statuses.set(server.name, { connected: true, toolCount: tools.length });
49284
+ return { client, tools };
49285
+ })();
49286
+ attempt.catch(() => {
49287
+ });
49288
+ try {
49289
+ const outcome = await Promise.race([attempt, deadline]);
49290
+ if (outcome === "timeout") {
49291
+ this.statuses.set(server.name, { connected: false, toolCount: 0, error: `timed out after ${timeoutMs / 1e3}s` });
49292
+ } else {
49293
+ this.connected.set(server.name, outcome);
49294
+ this.statuses.set(server.name, { connected: true, toolCount: outcome.tools.length });
49295
+ }
49071
49296
  } catch (err) {
49072
49297
  this.statuses.set(server.name, {
49073
49298
  connected: false,
49074
49299
  toolCount: 0,
49075
49300
  error: err instanceof Error ? err.message : String(err)
49076
49301
  });
49302
+ } finally {
49303
+ clearTimeout(timer);
49077
49304
  }
49305
+ const settled = this.statuses.get(server.name);
49306
+ if (settled) onServer?.({ server: server.name, ...settled });
49078
49307
  }
49079
49308
  /** Every tool from every connected server, paired with the server it came
49080
49309
  * from, in CONFIGURED order (`serverOrder`) -- not the order servers
@@ -49110,7 +49339,12 @@ var McpRegistry = class {
49110
49339
  * that records a failure without a reason, because "no tools" alone tells
49111
49340
  * the reader nothing about what to fix. */
49112
49341
  status() {
49113
- return Array.from(this.statuses, ([server, s2]) => ({ server, ...s2 }));
49342
+ const out = [];
49343
+ for (const server of this.serverOrder) {
49344
+ const s2 = this.statuses.get(server);
49345
+ if (s2) out.push({ server, ...s2 });
49346
+ }
49347
+ return out;
49114
49348
  }
49115
49349
  };
49116
49350
 
@@ -49755,11 +49989,11 @@ async function main() {
49755
49989
  const procs = args.allowShell ? createNodeProcs({ roots: args.roots, log: slog }) : void 0;
49756
49990
  const mcpPolicy = buildMcpPolicy(fileConfig);
49757
49991
  const mcpRegistry = new McpRegistry(args.stateDir);
49758
- await mcpRegistry.connectAll();
49992
+ slog("mcp: connecting configured servers\u2026");
49993
+ await mcpRegistry.connectAll({
49994
+ onServer: (s2) => slog(s2.connected ? `mcp: "${s2.server}" connected -- ${s2.toolCount} tool(s)` : `mcp: "${s2.server}" not connected -- ${s2.error} (run: corenel mcp doctor)`)
49995
+ });
49759
49996
  const unregisterMcpTools = registerMcpTools(mcpRegistry, mcpPolicy);
49760
- for (const s2 of mcpRegistry.status()) {
49761
- slog(s2.connected ? `mcp: "${s2.server}" connected -- ${s2.toolCount} tool(s)` : `mcp: "${s2.server}" not connected -- ${s2.error} (run: corenel mcp doctor)`);
49762
- }
49763
49997
  const mcpPort = buildMcpPort(mcpRegistry, mcpPolicy);
49764
49998
  const mcpAdminPort = buildMcpAdmin(args.stateDir);
49765
49999
  const sessions = runner ? await wireSessions({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corenel/sidecar",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "type": "module",
5
5
  "license": "Elastic-2.0",
6
6
  "description": "Prompd sidecar — a local capability server. Run it next to Prompd (prompd.app) so the browser app can reach your machine: local LLMs (Ollama/LM Studio/vLLM) without CORS, your filesystem, and (opt-in) shell.",
@@ -36,10 +36,10 @@
36
36
  "esbuild": "^0.21.5",
37
37
  "tsx": "^4.19.2",
38
38
  "typescript": "^5.9.3",
39
- "@corenel/harness": "0.7.0",
40
- "@corenel/protocol": "0.7.0",
41
- "@corenel/crew": "0.0.0",
42
- "@corenel/tools-node": "0.3.2"
39
+ "@corenel/protocol": "0.7.1",
40
+ "@corenel/harness": "0.7.1",
41
+ "@corenel/tools-node": "0.3.3",
42
+ "@corenel/crew": "0.0.0"
43
43
  },
44
44
  "repository": {
45
45
  "type": "git",