alegra-data-agent 0.4.2 → 0.4.4

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.
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import { AuthRequiredError, fetchManifest } from "../api.js";
5
5
  import { readAuth } from "../auth.js";
6
6
  import { bold, cyan, dim, gray, green, log, logError, logWarn, yellow } from "../colors.js";
7
- import { detectDrift, inspectMcpInTarget } from "../installer.js";
7
+ import { detectDrift, findMisplacedMcps, inspectMcpInTarget } from "../installer.js";
8
8
  import { COMMAND_NAME, GATEWAY_URL, getAutoUpdateLogPath, getClaudeJsonPath, getClaudeSkillsDir, getLockFilePath, getMcpScopeDir, getMcpScopeWarning, getPackageVersion, getTargetSkillsDir, readLockFile, } from "../lib.js";
9
9
  import { TARGETS } from "../targets.js";
10
10
  function reportAutoUpdateLog() {
@@ -145,6 +145,14 @@ export function findRogueMetabase(manifest, specs = Object.values(TARGETS)) {
145
145
  }
146
146
  return found;
147
147
  }
148
+ function reportMisplacedMcps(manifest) {
149
+ const found = findMisplacedMcps(manifest);
150
+ if (found.length === 0)
151
+ return;
152
+ logWarn(`Hay ${found.length} MCP(s) de Alegra fuera de su lugar: ${found.join(" · ")}`);
153
+ log(gray(` El próximo arranque los retira solo. Para hacerlo ya: ${COMMAND_NAME} update`));
154
+ log("");
155
+ }
148
156
  function reportRogueMetabase(manifest) {
149
157
  const official = manifest.mcps?.metabase?.url;
150
158
  if (!official)
@@ -320,6 +328,7 @@ export async function runStatus() {
320
328
  log("");
321
329
  reportMcps(manifest, lock);
322
330
  reportRogueMetabase(manifest);
331
+ reportMisplacedMcps(manifest);
323
332
  reportClaudeAiDuplicates(manifest);
324
333
  reportAccountConnectorOverlap(manifest);
325
334
  const total = drift.toInstall.length +
@@ -97,7 +97,9 @@ async function runUpdateOnce(opts) {
97
97
  for (const name of drift.mcps.toUpdate) {
98
98
  const motivo = drift.mcps.missing.includes(name)
99
99
  ? "(MCP ausente o descartado por el cliente — se repone)"
100
- : "(MCP actualizado)";
100
+ : drift.mcps.outOfPlace.includes(name)
101
+ ? "(copia fuera de lugar o proxy ausente — se corrige)"
102
+ : "(MCP actualizado)";
101
103
  log(` ${yellow("↻")} ${name} ${gray(motivo)}`);
102
104
  }
103
105
  log("");
package/dist/installer.js CHANGED
@@ -4,8 +4,8 @@ import { chmodSync, copyFileSync, existsSync, readFileSync, readdirSync, renameS
4
4
  import { basename, dirname, join, resolve, sep } from "node:path";
5
5
  import { AuthRequiredError, fetchManifest, fetchSkillFile } from "./api.js";
6
6
  import { dim, gray, green, logError, red, yellow } from "./colors.js";
7
- import { ensureLazyProxy, lazyMcpDir, stdioLaunch } from "./lazy-mcp.js";
8
- import { ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getCursorLegacyRulesDir, getHermesBin, getLockFilePath, getMcpScopeDir, getOpenCodeLegacyPromptsDir, getPackageVersion, readLockFile, sha256File, writeLockFile, } from "./lib.js";
7
+ import { ensureLazyProxy, lazyMcpDir, lazyProxyPath, lazyToolsPath, stdioLaunch, } from "./lazy-mcp.js";
8
+ import { ensureDir, getAntigravityGeminiLegacyMcpConfigPath, getAntigravityGeminiLegacySkillsDir, getAntigravityLegacyMcpConfigPath, getAntigravityLegacySkillsDir, getClaudeSettingsPath, getCursorLegacyRulesDir, getHermesBin, getLockFilePath, getMcpScopeDir, getMcpScopeWarning, getOpenCodeLegacyPromptsDir, getPackageVersion, getUserScopeMcpConfigPath, isMcpScopeEnvOverride, readLockFile, sha256File, writeLockFile, } from "./lib.js";
9
9
  import { inlineReferences, renderHermesMcpOps, renderMcpForTarget, } from "./render.js";
10
10
  import { TARGETS, detectInstalledTargets } from "./targets.js";
11
11
  const SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
@@ -288,10 +288,9 @@ export function updateJsonFile(path, mutate) {
288
288
  }
289
289
  throw new Error(`${path} cambió 5 veces mientras se escribía; volvé a correr la instalación.`);
290
290
  }
291
- function setClaudeProjectMcpApproval(name, spec, approved) {
291
+ function setClaudeProjectMcpApproval(name, spec, approved, dir = getMcpScopeDir()) {
292
292
  if (spec.id !== "claude-code")
293
293
  return;
294
- const dir = getMcpScopeDir();
295
294
  if (!dir)
296
295
  return;
297
296
  const settingsPath = join(dir, ".claude", "settings.json");
@@ -311,6 +310,134 @@ function setClaudeProjectMcpApproval(name, spec, approved) {
311
310
  catch {
312
311
  }
313
312
  }
313
+ export function isOwnMcpEntry(entry, mcp, spec, recordedHash) {
314
+ if (!entry || typeof entry !== "object")
315
+ return false;
316
+ const got = stableStringify(entry);
317
+ if (recordedHash && createHash("sha256").update(got).digest("hex") === recordedHash)
318
+ return true;
319
+ const shapes = [mcp, { ...mcp, lazy: undefined }];
320
+ return shapes.some((m) => stableStringify(renderMcpForTarget(m, spec)) === got);
321
+ }
322
+ function removeMcpEntryAt(path, rootKey, name, isOurs) {
323
+ if (!existsSync(path))
324
+ return false;
325
+ return updateJsonFile(path, (cfg) => {
326
+ const section = cfg[rootKey];
327
+ if (!section || !(name in section) || !isOurs(section[name]))
328
+ return false;
329
+ delete section[name];
330
+ return true;
331
+ });
332
+ }
333
+ function canReconcileScope() {
334
+ return !getMcpScopeWarning() && !isMcpScopeEnvOverride();
335
+ }
336
+ function staleMcpConfigPaths(spec, prev) {
337
+ if (!canReconcileScope())
338
+ return [];
339
+ const out = new Set();
340
+ const recorded = prev?.configPaths?.[spec.id];
341
+ if (recorded)
342
+ out.add(recorded);
343
+ const user = getUserScopeMcpConfigPath(spec.id);
344
+ if (user && getMcpScopeDir())
345
+ out.add(user);
346
+ out.delete(spec.mcpConfigPath);
347
+ return [...out];
348
+ }
349
+ function retireStaleMcpCopies(mcp, specs, prev) {
350
+ const touched = [];
351
+ for (const spec of specs) {
352
+ if (spec.mcpMechanism === "hermes-cli" || spec.mcpMechanism === "toml-block")
353
+ continue;
354
+ for (const path of staleMcpConfigPaths(spec, prev)) {
355
+ try {
356
+ const isOurs = (e) => isOwnMcpEntry(e, mcp, spec, prev?.renderedHash?.[spec.id]);
357
+ if (removeMcpEntryAt(path, spec.mcpRootKey, mcp.name, isOurs)) {
358
+ touched.push(path);
359
+ if (spec.id === "claude-code" && basename(path) === ".mcp.json") {
360
+ setClaudeProjectMcpApproval(mcp.name, spec, false, dirname(path));
361
+ }
362
+ }
363
+ }
364
+ catch {
365
+ }
366
+ }
367
+ }
368
+ return touched;
369
+ }
370
+ function retireFromUnscopableTargets(mcp, specs, prev) {
371
+ const out = [];
372
+ for (const spec of specs) {
373
+ try {
374
+ if (spec.mcpMechanism === "hermes-cli" || spec.mcpMechanism === "toml-block") {
375
+ if (prev?.targets?.includes(spec.id) && unregisterMcpInTarget(mcp.name, spec))
376
+ out.push(spec.id);
377
+ }
378
+ else if (removeMcpEntryAt(spec.mcpConfigPath, spec.mcpRootKey, mcp.name, (e) => isOwnMcpEntry(e, mcp, spec, prev?.renderedHash?.[spec.id]))) {
379
+ out.push(spec.id);
380
+ }
381
+ }
382
+ catch {
383
+ }
384
+ }
385
+ return out;
386
+ }
387
+ export function mcpOutOfPlace(mcp, prev) {
388
+ const found = [];
389
+ if (mcp.delivery === "connector")
390
+ return found;
391
+ if (mcp.lazy && prev && (!existsSync(lazyProxyPath()) || !existsSync(lazyToolsPath(mcp.name)))) {
392
+ found.push("falta el proxy de arranque diferido");
393
+ }
394
+ const scoped = Boolean(getMcpScopeDir()) && canReconcileScope();
395
+ for (const spec of Object.values(TARGETS)) {
396
+ if (mcp.targets && !mcp.targets.includes(spec.id))
397
+ continue;
398
+ if (spec.mcpMechanism === "hermes-cli" || spec.mcpMechanism === "toml-block") {
399
+ if (scoped && prev?.targets?.includes(spec.id))
400
+ found.push(`copia en ${spec.id}`);
401
+ continue;
402
+ }
403
+ const candidates = getUserScopeMcpConfigPath(spec.id)
404
+ ? staleMcpConfigPaths(spec, prev)
405
+ : scoped
406
+ ? [spec.mcpConfigPath]
407
+ : [];
408
+ for (const path of candidates) {
409
+ try {
410
+ if (!existsSync(path))
411
+ continue;
412
+ const cfg = JSON.parse(readFileSync(path, "utf-8"));
413
+ const entry = cfg[spec.mcpRootKey]?.[mcp.name];
414
+ if (entry && isOwnMcpEntry(entry, mcp, spec, prev?.renderedHash?.[spec.id])) {
415
+ found.push(`copia en ${path}`);
416
+ }
417
+ }
418
+ catch {
419
+ }
420
+ }
421
+ }
422
+ return found;
423
+ }
424
+ export function findMisplacedMcps(manifest) {
425
+ const lock = readLockFile();
426
+ const found = [];
427
+ for (const mcp of Object.values(manifest.mcps ?? {})) {
428
+ for (const what of mcpOutOfPlace(mcp, lock.mcps?.[mcp.name]))
429
+ found.push(`${mcp.name}: ${what}`);
430
+ }
431
+ return found;
432
+ }
433
+ function scopedConfigPaths(targetIds) {
434
+ const out = {};
435
+ for (const id of targetIds) {
436
+ if (getUserScopeMcpConfigPath(id))
437
+ out[id] = TARGETS[id].mcpConfigPath;
438
+ }
439
+ return out;
440
+ }
314
441
  function registerMcpInTarget(mcp, spec) {
315
442
  validateMcpName(mcp.name);
316
443
  if (spec.mcpMechanism === "hermes-cli") {
@@ -884,14 +1011,33 @@ export function installMcp(mcp, opts = {}) {
884
1011
  error: `entry local retirada — ${hint}`,
885
1012
  };
886
1013
  }
887
- const wantedTargets = resolveTargets(opts).filter((t) => !mcp.targets || mcp.targets.includes(t.id));
1014
+ const selected = resolveTargets(opts).filter((t) => !mcp.targets || mcp.targets.includes(t.id));
1015
+ const unscopable = getMcpScopeDir()
1016
+ ? selected.filter((t) => !getUserScopeMcpConfigPath(t.id))
1017
+ : [];
1018
+ const wantedTargets = selected.filter((t) => !unscopable.includes(t));
888
1019
  const targetIds = wantedTargets.map((t) => t.id);
1020
+ const retiredUnscoped = opts.dryRun || unscopable.length === 0
1021
+ ? []
1022
+ : retireFromUnscopableTargets(mcp, unscopable, lock.mcps[mcp.name]);
1023
+ const unscopedNote = unscopable.length > 0
1024
+ ? `con MCP_DIR solo va a Claude Code/Cursor; no se instala en ${unscopable.map((t) => t.id).join(", ")}${retiredUnscoped.length > 0 ? ` (retirado de ${retiredUnscoped.join(", ")})` : ""}`
1025
+ : undefined;
889
1026
  if (targetIds.length === 0) {
1027
+ const prevEntry = lock.mcps[mcp.name];
1028
+ if (prevEntry && retiredUnscoped.length > 0) {
1029
+ const left = (prevEntry.targets ?? []).filter((t) => !retiredUnscoped.includes(t));
1030
+ if (left.length > 0)
1031
+ lock.mcps[mcp.name] = { ...prevEntry, targets: left };
1032
+ else
1033
+ delete lock.mcps[mcp.name];
1034
+ writeLockFile(lock);
1035
+ }
890
1036
  return {
891
1037
  name: mcp.name,
892
1038
  kind: "mcp",
893
- status: "skipped",
894
- error: "ningún target soportado disponible",
1039
+ status: retiredUnscoped.length > 0 ? "updated" : "skipped",
1040
+ error: unscopedNote ?? "ningún target soportado disponible",
895
1041
  };
896
1042
  }
897
1043
  if (mcp.lazy && !opts.dryRun) {
@@ -922,6 +1068,27 @@ export function installMcp(mcp, opts = {}) {
922
1068
  .filter((t) => !writeTargets.includes(t) && !needsWrite(t))
923
1069
  .map((t) => t.id);
924
1070
  if (writeTargets.length === 0 && targetsMatch) {
1071
+ const retired = opts.dryRun ? [] : retireStaleMcpCopies(mcp, wantedTargets, prev);
1072
+ const configPaths = { ...(prev?.configPaths ?? {}), ...scopedConfigPaths(targetIds) };
1073
+ if (prev &&
1074
+ (retired.length > 0 ||
1075
+ stableStringify(configPaths) !== stableStringify(prev.configPaths ?? {}))) {
1076
+ lock.mcps[mcp.name] = { ...prev, configPaths };
1077
+ writeLockFile(lock);
1078
+ }
1079
+ if (retired.length > 0 || retiredUnscoped.length > 0) {
1080
+ const notes = [
1081
+ retired.length > 0 ? `copia de otro scope retirada de ${retired.join(", ")}` : "",
1082
+ retiredUnscoped.length > 0 ? (unscopedNote ?? "") : "",
1083
+ ].filter(Boolean);
1084
+ return {
1085
+ name: mcp.name,
1086
+ kind: "mcp",
1087
+ status: "updated",
1088
+ targets: targetIds,
1089
+ error: notes.join("; "),
1090
+ };
1091
+ }
925
1092
  return { name: mcp.name, kind: "mcp", status: "unchanged", targets: targetIds };
926
1093
  }
927
1094
  if (opts.dryRun) {
@@ -953,6 +1120,7 @@ export function installMcp(mcp, opts = {}) {
953
1120
  error: failures.join("; "),
954
1121
  };
955
1122
  }
1123
+ const retired = retireStaleMcpCopies(mcp, okTargets.map((id) => TARGETS[id]), prev);
956
1124
  const renderedHash = { ...(prev?.renderedHash ?? {}) };
957
1125
  for (const t of writeTargets) {
958
1126
  if (okTargets.includes(t.id))
@@ -960,7 +1128,10 @@ export function installMcp(mcp, opts = {}) {
960
1128
  }
961
1129
  const outOfScopeTargets = configChanged
962
1130
  ? []
963
- : (prev?.targets ?? []).filter((t) => !targetIds.includes(t) && (!mcp.targets || mcp.targets.includes(t)));
1131
+ : (prev?.targets ?? []).filter((t) => !targetIds.includes(t) &&
1132
+ (!mcp.targets || mcp.targets.includes(t)) &&
1133
+ !retiredUnscoped.includes(t) &&
1134
+ !(getMcpScopeDir() && !getUserScopeMcpConfigPath(t)));
964
1135
  const lockTargets = [...new Set([...outOfScopeTargets, ...okTargets])];
965
1136
  lock.mcps[mcp.name] = {
966
1137
  name: mcp.name,
@@ -969,6 +1140,7 @@ export function installMcp(mcp, opts = {}) {
969
1140
  configHash: mcpConfigHash(mcp),
970
1141
  targets: lockTargets,
971
1142
  renderedHash,
1143
+ configPaths: { ...(prev?.configPaths ?? {}), ...scopedConfigPaths(okTargets) },
972
1144
  };
973
1145
  writeLockFile(lock);
974
1146
  return {
@@ -976,7 +1148,13 @@ export function installMcp(mcp, opts = {}) {
976
1148
  kind: "mcp",
977
1149
  status: prev ? "updated" : "installed",
978
1150
  targets: okTargets,
979
- error: failures.length > 0 ? `targets con fallo — ${failures.join("; ")}` : undefined,
1151
+ error: [
1152
+ failures.length > 0 ? `targets con fallo — ${failures.join("; ")}` : "",
1153
+ retired.length > 0 ? `copia de otro scope retirada de ${retired.join(", ")}` : "",
1154
+ unscopedNote ?? "",
1155
+ ]
1156
+ .filter(Boolean)
1157
+ .join("; ") || undefined,
980
1158
  };
981
1159
  }
982
1160
  export async function installAll(opts = {}) {
@@ -1079,6 +1257,18 @@ export function uninstallMcp(name, opts = {}) {
1079
1257
  try {
1080
1258
  for (const tId of targetIds) {
1081
1259
  unregisterMcpInTarget(name, TARGETS[tId]);
1260
+ const recorded = prev.configPaths?.[tId];
1261
+ if (recorded && recorded !== TARGETS[tId].mcpConfigPath) {
1262
+ try {
1263
+ if (removeMcpEntryAt(recorded, TARGETS[tId].mcpRootKey, name, () => true)) {
1264
+ if (tId === "claude-code" && basename(recorded) === ".mcp.json") {
1265
+ setClaudeProjectMcpApproval(name, TARGETS[tId], false, dirname(recorded));
1266
+ }
1267
+ }
1268
+ }
1269
+ catch {
1270
+ }
1271
+ }
1082
1272
  }
1083
1273
  }
1084
1274
  catch (err) {
@@ -1091,6 +1281,7 @@ export function uninstallMcp(name, opts = {}) {
1091
1281
  }
1092
1282
  delete lock.mcps[name];
1093
1283
  writeLockFile(lock);
1284
+ rmSync(lazyToolsPath(name), { force: true });
1094
1285
  }
1095
1286
  return { name, kind: "mcp", status: "installed", targets: targetIds };
1096
1287
  }
@@ -1109,7 +1300,13 @@ export function uninstallAll(opts = {}) {
1109
1300
  catch {
1110
1301
  }
1111
1302
  }
1112
- rmSync(lazyMcpDir(), { recursive: true, force: true });
1303
+ if (Object.keys(readLockFile().mcps ?? {}).length === 0) {
1304
+ try {
1305
+ rmSync(lazyMcpDir(), { recursive: true, force: true });
1306
+ }
1307
+ catch {
1308
+ }
1309
+ }
1113
1310
  }
1114
1311
  return results;
1115
1312
  }
@@ -1142,6 +1339,7 @@ export function detectDrift(manifest) {
1142
1339
  unchanged: [],
1143
1340
  orphaned: [],
1144
1341
  missing: [],
1342
+ outOfPlace: [],
1145
1343
  };
1146
1344
  const manifestMcps = manifest.mcps ?? {};
1147
1345
  for (const name of Object.keys(manifestMcps)) {
@@ -1157,9 +1355,13 @@ export function detectDrift(manifest) {
1157
1355
  continue;
1158
1356
  }
1159
1357
  const missing = mcpNeedsRepairInTargets(manifestMcps[name], (prevMcp.targets ?? []), prevMcp.renderedHash);
1160
- if (missing.length > 0) {
1358
+ const outOfPlace = mcpOutOfPlace(manifestMcps[name], prevMcp);
1359
+ if (missing.length > 0 || outOfPlace.length > 0) {
1161
1360
  mcps.toUpdate.push(name);
1162
- mcps.missing.push(name);
1361
+ if (missing.length > 0)
1362
+ mcps.missing.push(name);
1363
+ if (outOfPlace.length > 0)
1364
+ mcps.outOfPlace.push(name);
1163
1365
  }
1164
1366
  else {
1165
1367
  mcps.unchanged.push(name);
package/dist/lazy-mcp.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { COMMAND_NAME, adaptStdioCommand } from "./lib.js";
@@ -25,12 +25,18 @@ export function ensureLazyProxy(mcp) {
25
25
  return;
26
26
  mkdirSync(lazyMcpDir(), { recursive: true });
27
27
  writeIfChanged(lazyProxyPath(), lazyProxySource());
28
- writeIfChanged(lazyToolsPath(mcp.name), `${JSON.stringify({ name: mcp.name, tools: mcp.lazy.tools }, null, 2)}\n`);
28
+ writeIfChanged(lazyToolsPath(mcp.name), `${JSON.stringify({
29
+ name: mcp.name,
30
+ ...(mcp.lazy.protocolVersion ? { protocolVersion: mcp.lazy.protocolVersion } : {}),
31
+ tools: mcp.lazy.tools,
32
+ }, null, 2)}\n`);
29
33
  }
30
34
  function writeIfChanged(path, content) {
31
35
  if (existsSync(path) && readFileSync(path, "utf-8") === content)
32
36
  return;
33
- writeFileSync(path, content, { mode: 0o644 });
37
+ const tmp = `${path}.${process.pid}.tmp`;
38
+ writeFileSync(tmp, content, { mode: 0o644 });
39
+ renameSync(tmp, path);
34
40
  }
35
41
  export function lazyProxySource() {
36
42
  const product = JSON.stringify(COMMAND_NAME);
@@ -38,7 +44,7 @@ export function lazyProxySource() {
38
44
  // Proxy de arranque diferido de ${COMMAND_NAME}. Generado por el instalador: no
39
45
  // editar, se reescribe en cada install/update. Uso:
40
46
  // node lazy-mcp-proxy.mjs <tools.json> -- <comando real> [args…]
41
- import { spawn } from "node:child_process";
47
+ import { spawn, spawnSync } from "node:child_process";
42
48
  import { readFileSync } from "node:fs";
43
49
  import { createInterface } from "node:readline";
44
50
 
@@ -51,13 +57,18 @@ if (sep !== 1 || argv.length < 3) {
51
57
  const spec = JSON.parse(readFileSync(argv[0], "utf8"));
52
58
  const [cmd, ...args] = argv.slice(2);
53
59
  const INIT_ID = "__alegra_lazy_init__";
60
+ const START_TIMEOUT_MS = 10 * 60 * 1000;
54
61
 
55
62
  const send = (m) => process.stdout.write(JSON.stringify(m) + "\\n");
63
+ const key = (id) => JSON.stringify(id);
56
64
  let initParams = null;
57
65
  let child = null;
58
66
  let ready = false;
59
67
  let dead = null;
68
+ let startTimer = null;
60
69
  const queue = [];
70
+ // Requests reenviadas al server que todavía no tuvieron respuesta.
71
+ const inflight = new Map();
61
72
 
62
73
  function errorFor(m, reason) {
63
74
  if (m.method === "tools/call") {
@@ -66,47 +77,73 @@ function errorFor(m, reason) {
66
77
  return { jsonrpc: "2.0", id: m.id, error: { code: -32603, message: reason } };
67
78
  }
68
79
 
80
+ function killChild() {
81
+ if (!child || child.exitCode !== null) return;
82
+ if (process.platform === "win32") {
83
+ spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
84
+ } else {
85
+ child.kill("SIGTERM");
86
+ }
87
+ }
88
+
69
89
  function die(reason) {
90
+ if (dead) return;
70
91
  dead = reason;
71
92
  ready = false;
93
+ clearTimeout(startTimer);
72
94
  for (const m of queue.splice(0)) send(errorFor(m, reason));
95
+ for (const m of inflight.values()) send(errorFor(m, reason));
96
+ inflight.clear();
97
+ killChild();
98
+ }
99
+
100
+ function forward(m) {
101
+ if (m.id !== undefined && m.id !== null && m.method) inflight.set(key(m.id), m);
102
+ child.stdin.write(JSON.stringify(m) + "\\n");
73
103
  }
74
104
 
75
105
  function start() {
76
106
  if (child || dead) return;
77
107
  child = spawn(cmd, args, { stdio: ["pipe", "pipe", "inherit"] });
108
+ startTimer = setTimeout(
109
+ () => die("El MCP " + spec.name + " tardó más de 10 min en arrancar (¿se cortó la descarga?). Reintentá en una sesión nueva."),
110
+ START_TIMEOUT_MS,
111
+ );
78
112
  child.on("error", (e) => die("No se pudo arrancar el MCP " + spec.name + ": " + e.message));
79
- child.on("exit", (code, signal) => {
80
- if (!dead) die("El MCP " + spec.name + " terminó (" + (signal ?? "código " + code) + "). Reintentá en una sesión nueva; si persiste, corré " + ${product} + " status.");
113
+ // "close" y no "exit": en "exit" el stdout del server todavía puede tener
114
+ // respuestas sin leer.
115
+ child.on("close", (code, signal) => {
116
+ die("El MCP " + spec.name + " terminó (" + (signal ?? "código " + code) + "). Reintentá en una sesión nueva; si persiste, corré " + ${product} + " status.");
81
117
  });
82
118
  createInterface({ input: child.stdout }).on("line", (line) => {
119
+ let m;
120
+ try { m = JSON.parse(line); } catch { m = null; }
83
121
  if (ready) {
122
+ if (m && m.id !== undefined && !m.method) inflight.delete(key(m.id));
84
123
  process.stdout.write(line + "\\n");
85
124
  return;
86
125
  }
87
- let m;
88
- try { m = JSON.parse(line); } catch { return; }
89
- if (m.id !== INIT_ID) return; // notificaciones previas al handshake
126
+ if (!m || m.id !== INIT_ID) return; // notificaciones previas al handshake
90
127
  if (m.error) {
91
128
  die("El MCP " + spec.name + " rechazó el handshake: " + (m.error.message ?? "sin detalle"));
92
- child.kill();
93
129
  return;
94
130
  }
95
131
  ready = true;
132
+ clearTimeout(startTimer);
96
133
  child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\\n");
97
- for (const q of queue.splice(0)) child.stdin.write(JSON.stringify(q) + "\\n");
134
+ for (const q of queue.splice(0)) forward(q);
98
135
  });
99
136
  child.stdin.on("error", () => {});
100
137
  child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: INIT_ID, method: "initialize", params: initParams }) + "\\n");
101
138
  }
102
139
 
103
140
  createInterface({ input: process.stdin }).on("line", (line) => {
141
+ let m;
142
+ try { m = JSON.parse(line); } catch { return; }
104
143
  if (ready) {
105
- child.stdin.write(line + "\\n");
144
+ forward(m);
106
145
  return;
107
146
  }
108
- let m;
109
- try { m = JSON.parse(line); } catch { return; }
110
147
  switch (m.method) {
111
148
  case "initialize":
112
149
  initParams = m.params ?? {};
@@ -114,7 +151,7 @@ createInterface({ input: process.stdin }).on("line", (line) => {
114
151
  jsonrpc: "2.0",
115
152
  id: m.id,
116
153
  result: {
117
- protocolVersion: initParams.protocolVersion ?? "2025-06-18",
154
+ protocolVersion: spec.protocolVersion ?? initParams.protocolVersion ?? "2025-06-18",
118
155
  capabilities: { tools: {} },
119
156
  serverInfo: { name: spec.name, version: "lazy" },
120
157
  },
@@ -135,6 +172,12 @@ createInterface({ input: process.stdin }).on("line", (line) => {
135
172
  case "prompts/list":
136
173
  send({ jsonrpc: "2.0", id: m.id, result: { prompts: [] } });
137
174
  return;
175
+ case "notifications/cancelled": {
176
+ // Una request cancelada mientras el server arranca no se reenvía.
177
+ const i = queue.findIndex((q) => key(q.id) === key(m.params?.requestId));
178
+ if (i >= 0) queue.splice(i, 1);
179
+ return;
180
+ }
138
181
  }
139
182
  if (m.id === undefined || m.id === null) return; // notificación: nada que contestar
140
183
  if (dead) {
@@ -145,9 +188,11 @@ createInterface({ input: process.stdin }).on("line", (line) => {
145
188
  start();
146
189
  });
147
190
 
148
- process.stdin.on("end", () => {
149
- if (child) child.kill();
191
+ function shutdown() {
192
+ killChild();
150
193
  process.exit(0);
151
- });
194
+ }
195
+ process.stdin.on("end", shutdown);
196
+ for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) process.on(sig, shutdown);
152
197
  `;
153
198
  }
package/dist/lib.js CHANGED
@@ -43,6 +43,9 @@ function readAgentConf() {
43
43
  catch {
44
44
  return {};
45
45
  }
46
+ return parseAgentConf(raw);
47
+ }
48
+ export function parseAgentConf(raw) {
46
49
  const out = {};
47
50
  for (const line0 of raw.split("\n")) {
48
51
  const line = line0.trim();
@@ -88,6 +91,12 @@ function resolveMcpScope() {
88
91
  }
89
92
  return { dir, warning: undefined };
90
93
  }
94
+ export function isMcpScopeEnvOverride() {
95
+ const env = process.env.ALEGRA_DATA_AGENT_MCP_DIR;
96
+ if (env === undefined)
97
+ return false;
98
+ return env.trim() !== (readAgentConf().MCP_DIR ?? "").trim();
99
+ }
91
100
  export function getMcpScopeWarning() {
92
101
  return resolveMcpScope().warning;
93
102
  }
@@ -108,7 +117,17 @@ export function getCursorLegacyRulesDir() {
108
117
  return join(homedir(), ".cursor", "rules");
109
118
  }
110
119
  export function getCursorMcpPath() {
111
- return mcpScopedPath(join(".cursor", "mcp.json"), join(homedir(), ".cursor", "mcp.json"));
120
+ return mcpScopedPath(join(".cursor", "mcp.json"), getCursorUserMcpPath());
121
+ }
122
+ export function getCursorUserMcpPath() {
123
+ return join(homedir(), ".cursor", "mcp.json");
124
+ }
125
+ export function getUserScopeMcpConfigPath(target) {
126
+ if (target === "claude-code")
127
+ return getClaudeJsonPath();
128
+ if (target === "cursor")
129
+ return getCursorUserMcpPath();
130
+ return undefined;
112
131
  }
113
132
  export function getOpenCodeSkillsDir() {
114
133
  const xdgHome = process.env.XDG_CONFIG_HOME;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alegra-data-agent",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "private": false,
5
5
  "description": "Agente de Data interno de Alegra. Instala skills + MCPs en tu cliente de IA con un comando.",
6
6
  "license": "UNLICENSED",