@corenel/sidecar 0.8.1 → 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 +360 -61
  2. package/package.json +4 -4
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.1";
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) {
@@ -46884,7 +47005,7 @@ async function readMcpCredential(stateDir, server) {
46884
47005
  function isUsableClient(v2) {
46885
47006
  if (typeof v2 !== "object" || v2 === null) return false;
46886
47007
  const r2 = v2;
46887
- return typeof r2.clientId === "string" && r2.clientId.length > 0 && typeof r2.authServer === "string" && r2.authServer.length > 0 && (r2.clientSecret === void 0 || typeof r2.clientSecret === "string");
47008
+ return typeof r2.clientId === "string" && r2.clientId.length > 0 && typeof r2.authServer === "string" && r2.authServer.length > 0 && (r2.clientSecret === void 0 || typeof r2.clientSecret === "string") && (r2.redirectPort === void 0 || typeof r2.redirectPort === "number" && Number.isInteger(r2.redirectPort) && r2.redirectPort > 0 && r2.redirectPort < 65536);
46888
47009
  }
46889
47010
  async function readMcpClient(stateDir, server) {
46890
47011
  const doc = await readAuthJson(stateDir);
@@ -46965,6 +47086,15 @@ function firstAuthServer(doc) {
46965
47086
  const first = list[0];
46966
47087
  return typeof first === "string" && first.length > 0 ? first : null;
46967
47088
  }
47089
+ function scopesFrom(doc) {
47090
+ const list = doc.scopes_supported;
47091
+ if (!Array.isArray(list) || list.length === 0) return void 0;
47092
+ return list.every((s2) => typeof s2 === "string") ? list : void 0;
47093
+ }
47094
+ function resourceResult(doc, authServer, mcpUrl) {
47095
+ const scopes = scopesFrom(doc);
47096
+ return { authServer, resource: resourceFor(doc, mcpUrl), ...scopes ? { scopes } : {} };
47097
+ }
46968
47098
  function resourceMetadataUrlFromHeader(headerValue) {
46969
47099
  if (!headerValue) return null;
46970
47100
  const match = /resource_metadata="([^"]+)"/.exec(headerValue);
@@ -46978,9 +47108,22 @@ async function discoverAuthServer(mcpUrl, fetchImpl = fetch) {
46978
47108
  const wellKnownUrl = new URL(PROTECTED_RESOURCE_PATH, origin).toString();
46979
47109
  const direct = await tryFetchJson(wellKnownUrl, fetchImpl);
46980
47110
  if (direct) {
46981
- const authServer2 = firstAuthServer(direct);
46982
- if (authServer2) return { authServer: authServer2, resource: resourceFor(direct, mcpUrl) };
47111
+ const authServer = firstAuthServer(direct);
47112
+ if (authServer) return resourceResult(direct, authServer, mcpUrl);
46983
47113
  }
47114
+ const viaHint = await hintedResource(mcpUrl, fetchImpl);
47115
+ if (viaHint) {
47116
+ const authServer = firstAuthServer(viaHint);
47117
+ if (authServer) return resourceResult(viaHint, authServer, mcpUrl);
47118
+ }
47119
+ const own = await tryFetchJson(authServerMetadataUrl(origin), fetchImpl);
47120
+ if (own && typeof own.authorization_endpoint === "string" && own.authorization_endpoint.length > 0) {
47121
+ const issuer = typeof own.issuer === "string" && own.issuer.length > 0 ? own.issuer : origin;
47122
+ return resourceResult(own, issuer, mcpUrl);
47123
+ }
47124
+ return null;
47125
+ }
47126
+ async function hintedResource(mcpUrl, fetchImpl) {
46984
47127
  let probe;
46985
47128
  try {
46986
47129
  probe = await fetchImpl(mcpUrl);
@@ -46990,10 +47133,7 @@ async function discoverAuthServer(mcpUrl, fetchImpl = fetch) {
46990
47133
  if (probe.status !== 401) return null;
46991
47134
  const hinted = resourceMetadataUrlFromHeader(probe.headers.get("www-authenticate"));
46992
47135
  if (!hinted) return null;
46993
- const viaHint = await tryFetchJson(hinted, fetchImpl);
46994
- if (!viaHint) return null;
46995
- const authServer = firstAuthServer(viaHint);
46996
- return authServer ? { authServer, resource: resourceFor(viaHint, mcpUrl) } : null;
47136
+ return tryFetchJson(hinted, fetchImpl);
46997
47137
  }
46998
47138
  function authServerMetadataUrl(authServer) {
46999
47139
  const u2 = new URL(authServer);
@@ -47252,13 +47392,14 @@ async function loginToMcpServer(stateDir, server, deps = {}) {
47252
47392
  message: `"${server.name}" does not advertise an OAuth authorization server. If it takes a static token instead, store that as a header.`
47253
47393
  };
47254
47394
  }
47255
- const meta = await discoverAuthMetadata(
47395
+ const asMeta = await discoverAuthMetadata(
47256
47396
  discovered.authServer,
47257
47397
  fetchImpl
47258
47398
  );
47259
- const port = await getFreePort();
47260
- const redirectUri = `http://localhost:${port}/callback`;
47399
+ const meta = discovered.scopes ? { ...asMeta, scopesSupported: discovered.scopes } : asMeta;
47261
47400
  const cached = await readMcpClient(stateDir, server.name);
47401
+ const port = cached?.redirectPort ?? await getFreePort();
47402
+ const redirectUri = `http://localhost:${port}/callback`;
47262
47403
  const registered2 = cached && cached.authServer === discovered.authServer ? { clientId: cached.clientId, ...cached.clientSecret ? { clientSecret: cached.clientSecret } : {} } : await registerClient(
47263
47404
  meta,
47264
47405
  redirectUri,
@@ -47276,7 +47417,7 @@ async function loginToMcpServer(stateDir, server, deps = {}) {
47276
47417
  ok: false,
47277
47418
  reason: "no-dcr",
47278
47419
  authServer: discovered.authServer,
47279
- message: `"${server.name}"'s authorization server (${discovered.authServer}) does not support dynamic client registration, and there is no configured client id to fall back to. Store a static token as a header instead, if the server offers one.`
47420
+ message: `"${server.name}"'s authorization server (${discovered.authServer}) supports OAuth but not dynamic client registration, so no client can be created on demand. Store a static token as a header instead -- a server in this position normally issues one from its own settings.`
47280
47421
  };
47281
47422
  }
47282
47423
  const pkce = makePkce();
@@ -47321,7 +47462,7 @@ async function loginToMcpServer(stateDir, server, deps = {}) {
47321
47462
  }
47322
47463
 
47323
47464
  // src/config/mcpCmd.ts
47324
- var USAGE2 = "Use: corenel mcp add <name> <url> [--hosted] | remove <name> | list | set-header <name> <header> <value> | login <name> | refresh <name>";
47465
+ var USAGE2 = "Use: corenel mcp add <name> <url> [--hosted] | remove <name> | list | set-header <name> <header> <value> | set-client <name> <client-id> [<client-secret>] --auth-server <url> [--redirect-port <port>] | login <name> | refresh <name>";
47325
47466
  function isHttpUrl(url) {
47326
47467
  try {
47327
47468
  const parsed = new URL(url);
@@ -47334,7 +47475,7 @@ async function runMcp(raw, io, deps = {}) {
47334
47475
  const argv = stripStateDir(raw);
47335
47476
  const sub = argv[0];
47336
47477
  const { corrupt } = await readDaemonConfig(io.stateDir);
47337
- const isWrite = sub === "add" || sub === "remove" || sub === "set-header" || sub === "login";
47478
+ const isWrite = sub === "add" || sub === "remove" || sub === "set-header" || sub === "set-client" || sub === "login";
47338
47479
  if (corrupt && isWrite) {
47339
47480
  io.err(`daemon.json in ${io.stateDir} is not valid JSON; fix or delete it before writing.`);
47340
47481
  return 2;
@@ -47389,6 +47530,51 @@ async function runMcp(raw, io, deps = {}) {
47389
47530
  if (corrupt) io.err(`daemon.json in ${io.stateDir} is not valid JSON; these are the defaults, not what the file says.`);
47390
47531
  return 0;
47391
47532
  }
47533
+ if (sub === "set-client") {
47534
+ const flagAt = argv.indexOf("--auth-server");
47535
+ const authServer = flagAt === -1 ? void 0 : argv[flagAt + 1];
47536
+ const portAt = argv.indexOf("--redirect-port");
47537
+ const portRaw = portAt === -1 ? void 0 : argv[portAt + 1];
47538
+ const consumed = /* @__PURE__ */ new Set([0, flagAt, flagAt + 1, portAt, portAt + 1]);
47539
+ const positional = argv.filter((a2, i2) => !consumed.has(i2));
47540
+ const name = positional[0];
47541
+ const clientId = positional[1];
47542
+ const clientSecret = positional[2];
47543
+ if (!name || !clientId) {
47544
+ io.err(`corenel mcp set-client needs a name and a client id. ${USAGE2}`);
47545
+ return 2;
47546
+ }
47547
+ if (!authServer || !isHttpUrl(authServer)) {
47548
+ io.err(`corenel mcp set-client needs --auth-server <url> -- the authorization server the client was registered with, copied from the service's own app settings. ${USAGE2}`);
47549
+ return 2;
47550
+ }
47551
+ let redirectPort;
47552
+ if (portRaw !== void 0) {
47553
+ const parsed = Number(portRaw);
47554
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed >= 65536) {
47555
+ io.err(`"${portRaw}" is not a usable port for --redirect-port. ${USAGE2}`);
47556
+ return 2;
47557
+ }
47558
+ redirectPort = parsed;
47559
+ }
47560
+ const servers = await readMcpServers(io.stateDir);
47561
+ if (!servers.some((s2) => s2.name === name)) {
47562
+ io.err(`"${name}" is not a configured MCP server. Add it first: corenel mcp add ${name} <url>`);
47563
+ return 2;
47564
+ }
47565
+ await writeMcpClient(io.stateDir, name, {
47566
+ clientId,
47567
+ ...clientSecret ? { clientSecret } : {},
47568
+ authServer,
47569
+ ...redirectPort ? { redirectPort } : {}
47570
+ });
47571
+ io.out(`Stored an OAuth client for "${name}" (client id ${clientId}${clientSecret ? ", with a secret" : ""}).`);
47572
+ if (redirectPort !== void 0) {
47573
+ io.out(`Register this redirect URL with the service: http://localhost:${redirectPort}/callback`);
47574
+ }
47575
+ io.out(`Run: corenel mcp login ${name}`);
47576
+ return 0;
47577
+ }
47392
47578
  if (sub === "set-header") {
47393
47579
  const name = argv[1];
47394
47580
  const header = argv[2];
@@ -47442,7 +47628,7 @@ async function runMcp(raw, io, deps = {}) {
47442
47628
  return 2;
47443
47629
  }
47444
47630
  if (outcome.reason === "no-dcr") {
47445
- io.err(`"${name}"'s authorization server (${outcome.authServer}) does not support dynamic client registration, and corenel mcp login has no configured client id to fall back to. Use: corenel mcp set-header ${name} <header> <value> instead, if the server also offers a static token.`);
47631
+ io.err(`"${name}"'s authorization server (${outcome.authServer}) supports OAuth but not dynamic client registration, so no client can be created on demand. Use: corenel mcp set-header ${name} <header> <value> with a token from the service's own settings.`);
47446
47632
  return 2;
47447
47633
  }
47448
47634
  io.err(`Sign-in for "${name}" failed: ${outcome.message}`);
@@ -48849,7 +49035,8 @@ var McpHttpClient = class {
48849
49035
  * loop -- a credential that fails twice is refused, not flaky. */
48850
49036
  async send(method, params, isRetry = false) {
48851
49037
  const headers = await this.buildHeaders();
48852
- const res = await this.request(headers, method, params);
49038
+ const id = this.nextRequestId++;
49039
+ const res = await this.request(headers, id, method, params);
48853
49040
  if (res.status === 401) {
48854
49041
  if (!isRetry && await this.auth.onUnauthorized()) {
48855
49042
  return this.send(method, params, true);
@@ -48860,26 +49047,77 @@ var McpHttpClient = class {
48860
49047
  }
48861
49048
  const sessionId = res.headers.get(SESSION_HEADER);
48862
49049
  if (sessionId) this.sessionId = sessionId;
49050
+ if (!res.ok) {
49051
+ throw new Error(`MCP server "${this.server.name}" returned HTTP ${res.status}`);
49052
+ }
48863
49053
  const contentType = res.headers.get("content-type") ?? "";
49054
+ let body2;
48864
49055
  if (contentType.includes("text/event-stream")) {
48865
- throw new Error(
48866
- `MCP server "${this.server.name}" answered with an SSE stream (content-type: ${contentType}); streaming MCP responses are not supported yet`
48867
- );
48868
- }
48869
- 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 {
48870
49060
  throw new Error(
48871
49061
  `MCP server "${this.server.name}" answered with an unexpected content-type: "${contentType || "(none)"}"`
48872
49062
  );
48873
49063
  }
48874
- if (!res.ok) {
48875
- throw new Error(`MCP server "${this.server.name}" returned HTTP ${res.status}`);
48876
- }
48877
- const body2 = await res.json();
48878
49064
  if (body2.error) {
48879
49065
  throw new Error(`MCP server "${this.server.name}" returned error ${body2.error.code}: ${body2.error.message}`);
48880
49066
  }
48881
49067
  return body2.result;
48882
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
+ }
48883
49121
  /** The bare `fetch()` call, wrapped -- every OTHER throw in `send` already
48884
49122
  * names the server; this is the one call whose own failures (DNS miss,
48885
49123
  * connection refused, TLS failure) never reach `send`'s response-handling
@@ -48889,12 +49127,12 @@ var McpHttpClient = class {
48889
49127
  * whose entire job is diagnosis. `err.cause` carries the real reason
48890
49128
  * (ECONNREFUSED, ENOTFOUND, a TLS alert); undici puts it there rather than
48891
49129
  * in `err.message`. */
48892
- async request(headers, method, params) {
49130
+ async request(headers, id, method, params) {
48893
49131
  try {
48894
49132
  return await this.fetchImpl(this.server.url, {
48895
49133
  method: "POST",
48896
49134
  headers,
48897
- body: JSON.stringify({ jsonrpc: "2.0", id: this.nextRequestId++, method, params })
49135
+ body: JSON.stringify({ jsonrpc: "2.0", id, method, params })
48898
49136
  });
48899
49137
  } catch (err) {
48900
49138
  const cause = err instanceof Error && err.cause instanceof Error ? err.cause.message : err instanceof Error ? err.message : String(err);
@@ -48955,6 +49193,25 @@ function authProviderFor(stateDir, server, fetchImpl = fetch, accountToken = asy
48955
49193
 
48956
49194
  // src/mcp/registry.ts
48957
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
48958
49215
  async function accountTokenFor(stateDir) {
48959
49216
  try {
48960
49217
  const cred = await resolveCredential(process.env, stateDir);
@@ -48964,8 +49221,12 @@ async function accountTokenFor(stateDir) {
48964
49221
  }
48965
49222
  }
48966
49223
  function defaultClientFactory(stateDir, server) {
48967
- 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
+ );
48968
49228
  }
49229
+ var SERVER_CONNECT_TIMEOUT_MS = 3e4;
48969
49230
  var McpRegistry = class {
48970
49231
  constructor(stateDir, clientFactory = defaultClientFactory) {
48971
49232
  this.stateDir = stateDir;
@@ -48989,27 +49250,60 @@ var McpRegistry = class {
48989
49250
  * a server that fails to initialize, or fails listing tools, is recorded
48990
49251
  * in `status()` with zero tools and the daemon keeps starting.
48991
49252
  */
48992
- async connectAll() {
49253
+ async connectAll(opts = {}) {
48993
49254
  this.connected.clear();
48994
49255
  this.statuses.clear();
48995
49256
  const servers = await readMcpServers(this.stateDir);
48996
49257
  this.serverOrder = servers.map((s2) => s2.name);
48997
- 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)));
48998
49260
  }
48999
- async connectOne(server) {
49000
- 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 () => {
49001
49281
  const client = this.clientFactory(this.stateDir, server);
49002
49282
  await client.initialize();
49003
49283
  const tools = await client.listTools();
49004
- this.connected.set(server.name, { client, tools });
49005
- 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
+ }
49006
49296
  } catch (err) {
49007
49297
  this.statuses.set(server.name, {
49008
49298
  connected: false,
49009
49299
  toolCount: 0,
49010
49300
  error: err instanceof Error ? err.message : String(err)
49011
49301
  });
49302
+ } finally {
49303
+ clearTimeout(timer);
49012
49304
  }
49305
+ const settled = this.statuses.get(server.name);
49306
+ if (settled) onServer?.({ server: server.name, ...settled });
49013
49307
  }
49014
49308
  /** Every tool from every connected server, paired with the server it came
49015
49309
  * from, in CONFIGURED order (`serverOrder`) -- not the order servers
@@ -49045,7 +49339,12 @@ var McpRegistry = class {
49045
49339
  * that records a failure without a reason, because "no tools" alone tells
49046
49340
  * the reader nothing about what to fix. */
49047
49341
  status() {
49048
- 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;
49049
49348
  }
49050
49349
  };
49051
49350
 
@@ -49690,11 +49989,11 @@ async function main() {
49690
49989
  const procs = args.allowShell ? createNodeProcs({ roots: args.roots, log: slog }) : void 0;
49691
49990
  const mcpPolicy = buildMcpPolicy(fileConfig);
49692
49991
  const mcpRegistry = new McpRegistry(args.stateDir);
49693
- 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
+ });
49694
49996
  const unregisterMcpTools = registerMcpTools(mcpRegistry, mcpPolicy);
49695
- for (const s2 of mcpRegistry.status()) {
49696
- slog(s2.connected ? `mcp: "${s2.server}" connected -- ${s2.toolCount} tool(s)` : `mcp: "${s2.server}" not connected -- ${s2.error} (run: corenel mcp doctor)`);
49697
- }
49698
49997
  const mcpPort = buildMcpPort(mcpRegistry, mcpPolicy);
49699
49998
  const mcpAdminPort = buildMcpAdmin(args.stateDir);
49700
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.1",
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,9 +36,9 @@
36
36
  "esbuild": "^0.21.5",
37
37
  "tsx": "^4.19.2",
38
38
  "typescript": "^5.9.3",
39
- "@corenel/protocol": "0.7.0",
40
- "@corenel/tools-node": "0.3.2",
41
- "@corenel/harness": "0.7.0",
39
+ "@corenel/protocol": "0.7.1",
40
+ "@corenel/harness": "0.7.1",
41
+ "@corenel/tools-node": "0.3.3",
42
42
  "@corenel/crew": "0.0.0"
43
43
  },
44
44
  "repository": {