@elisym/mcp 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,9 @@ npx @elisym/mcp install --agent <agent-name>
20
20
  # List detected MCP clients
21
21
  npx @elisym/mcp install --list
22
22
 
23
+ # Refresh the version pin in installed clients (preserves agent + env)
24
+ npx @elisym/mcp update
25
+
23
26
  # Remove from MCP clients
24
27
  npx @elisym/mcp uninstall
25
28
 
package/dist/index.js CHANGED
@@ -319,6 +319,35 @@ function payment() {
319
319
  }
320
320
 
321
321
  // src/install.ts
322
+ function elisymPackageArgs() {
323
+ return ["-y", `@elisym/mcp@~${PACKAGE_VERSION}`];
324
+ }
325
+ function validateClientName(name) {
326
+ if (name === void 0) {
327
+ return;
328
+ }
329
+ if (!CLIENTS.some((c) => c.name === name)) {
330
+ throw new Error(
331
+ `Unknown client "${name}". Known clients: ${CLIENTS.map((c) => c.name).join(", ")}`
332
+ );
333
+ }
334
+ }
335
+ async function safeRewriteJson(path, expectedRaw, newConfig) {
336
+ let recheck;
337
+ try {
338
+ recheck = await readFile(path, "utf-8");
339
+ } catch (err) {
340
+ throw new Error(
341
+ `${path} disappeared between read and write: ${err.message}. Re-run after the file is restored.`
342
+ );
343
+ }
344
+ if (recheck !== expectedRaw) {
345
+ throw new Error(
346
+ `${path} was modified by another process during update. Close the MCP client and re-run.`
347
+ );
348
+ }
349
+ await writeJsonAtomic(path, newConfig);
350
+ }
322
351
  var CLIENTS = [
323
352
  {
324
353
  name: "claude-desktop",
@@ -361,7 +390,7 @@ var CLIENTS = [
361
390
  function buildServerEntry(agentName, env) {
362
391
  const entry = {
363
392
  command: "npx",
364
- args: ["-y", `@elisym/mcp@~${PACKAGE_VERSION}`]
393
+ args: elisymPackageArgs()
365
394
  };
366
395
  const mergedEnv = { ...env };
367
396
  if (agentName) {
@@ -373,6 +402,7 @@ function buildServerEntry(agentName, env) {
373
402
  return entry;
374
403
  }
375
404
  async function runInstall(options) {
405
+ validateClientName(options.client);
376
406
  if (options.agent) {
377
407
  validateAgentName(options.agent);
378
408
  }
@@ -402,7 +432,12 @@ async function runInstall(options) {
402
432
  console.log("No MCP clients found to install into.");
403
433
  }
404
434
  }
405
- async function runUninstall(options) {
435
+ async function runUpdate(options) {
436
+ validateClientName(options.client);
437
+ if (options.agent) {
438
+ validateAgentName(options.agent);
439
+ }
440
+ let updated = 0;
406
441
  for (const client of CLIENTS) {
407
442
  if (options.client && client.name !== options.client) {
408
443
  continue;
@@ -411,15 +446,105 @@ async function runUninstall(options) {
411
446
  if (!path) {
412
447
  continue;
413
448
  }
449
+ let raw;
414
450
  try {
415
- const raw = await readFile(path, "utf-8");
416
- const config = JSON.parse(raw);
417
- if (config.mcpServers?.elisym) {
418
- delete config.mcpServers.elisym;
419
- await writeJsonAtomic(path, config);
420
- console.log(`Removed from ${client.name}: ${path}`);
451
+ raw = await readFile(path, "utf-8");
452
+ } catch (err) {
453
+ if (err.code !== "ENOENT") {
454
+ console.log(`Skipped ${client.name}: ${err.message}`);
455
+ }
456
+ continue;
457
+ }
458
+ let config;
459
+ try {
460
+ config = JSON.parse(raw);
461
+ } catch {
462
+ console.log(`Warning: ${path} is not valid JSON. Skipping update for ${client.name}.`);
463
+ continue;
464
+ }
465
+ const existing = config.mcpServers?.elisym;
466
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
467
+ continue;
468
+ }
469
+ const rawEnv = existing.env;
470
+ const newEnv = rawEnv && typeof rawEnv === "object" && !Array.isArray(rawEnv) ? { ...rawEnv } : {};
471
+ const existingAgentRaw = typeof newEnv.ELISYM_AGENT === "string" ? newEnv.ELISYM_AGENT : void 0;
472
+ if (existingAgentRaw !== void 0 && options.agent === void 0) {
473
+ try {
474
+ validateAgentName(existingAgentRaw);
475
+ } catch (err) {
476
+ console.log(
477
+ `Skipped ${client.name}: existing ELISYM_AGENT in ${path} is invalid (${err.message}). Re-run with --agent <name> to overwrite.`
478
+ );
479
+ continue;
480
+ }
481
+ }
482
+ if (options.agent !== void 0) {
483
+ newEnv.ELISYM_AGENT = options.agent;
484
+ }
485
+ const entry = existing;
486
+ const newArgs = elisymPackageArgs();
487
+ if (Array.isArray(entry.args)) {
488
+ const idx = entry.args.findIndex(
489
+ (a) => typeof a === "string" && a.startsWith("@elisym/mcp@")
490
+ );
491
+ if (idx >= 0) {
492
+ entry.args[idx] = newArgs[1];
493
+ } else {
494
+ entry.args = newArgs;
421
495
  }
496
+ } else {
497
+ entry.args = newArgs;
498
+ }
499
+ if (Object.keys(newEnv).length > 0) {
500
+ entry.env = newEnv;
501
+ } else {
502
+ delete entry.env;
503
+ }
504
+ try {
505
+ await safeRewriteJson(path, raw, config);
506
+ } catch (err) {
507
+ console.log(`Skipped ${client.name}: ${err.message}`);
508
+ continue;
509
+ }
510
+ console.log(`Updated ${client.name}: ${path} -> @elisym/mcp@~${PACKAGE_VERSION}`);
511
+ updated++;
512
+ }
513
+ if (updated === 0) {
514
+ console.log("No existing elisym MCP installs found to update.");
515
+ }
516
+ }
517
+ async function runUninstall(options) {
518
+ validateClientName(options.client);
519
+ for (const client of CLIENTS) {
520
+ if (options.client && client.name !== options.client) {
521
+ continue;
522
+ }
523
+ const path = client.configPath();
524
+ if (!path) {
525
+ continue;
526
+ }
527
+ let raw;
528
+ try {
529
+ raw = await readFile(path, "utf-8");
530
+ } catch {
531
+ continue;
532
+ }
533
+ let config;
534
+ try {
535
+ config = JSON.parse(raw);
422
536
  } catch {
537
+ continue;
538
+ }
539
+ if (!config.mcpServers?.elisym) {
540
+ continue;
541
+ }
542
+ delete config.mcpServers.elisym;
543
+ try {
544
+ await safeRewriteJson(path, raw, config);
545
+ console.log(`Removed from ${client.name}: ${path}`);
546
+ } catch (err) {
547
+ console.log(`Skipped ${client.name}: ${err.message}`);
423
548
  }
424
549
  }
425
550
  }
@@ -444,18 +569,21 @@ async function writeJsonAtomic(path, data) {
444
569
  await writeFileAtomic(path, JSON.stringify(data, null, 2), 384);
445
570
  }
446
571
  async function installToConfig(path, entry) {
447
- let config;
448
572
  let raw;
449
573
  try {
450
574
  raw = await readFile(path, "utf-8");
575
+ } catch (err) {
576
+ if (err.code !== "ENOENT") {
577
+ throw err;
578
+ }
579
+ await writeJsonAtomic(path, { mcpServers: { elisym: entry } });
580
+ return true;
581
+ }
582
+ let config;
583
+ try {
451
584
  config = JSON.parse(raw);
452
585
  } catch {
453
- if (raw !== void 0) {
454
- console.error(
455
- `Warning: ${path} is not valid JSON. A backup was saved to ${path}.elisym-backup. Other MCP server entries may have been lost - restore from backup if needed.`
456
- );
457
- }
458
- config = {};
586
+ throw new Error(`${path} is not valid JSON. Fix the file manually and re-run install.`);
459
587
  }
460
588
  if (!config.mcpServers) {
461
589
  config.mcpServers = {};
@@ -463,14 +591,8 @@ async function installToConfig(path, entry) {
463
591
  if (config.mcpServers.elisym) {
464
592
  return false;
465
593
  }
466
- if (raw) {
467
- try {
468
- await writeFile(`${path}.elisym-backup`, raw, { mode: 384 });
469
- } catch {
470
- }
471
- }
472
594
  config.mcpServers.elisym = entry;
473
- await writeJsonAtomic(path, config);
595
+ await safeRewriteJson(path, raw, config);
474
596
  return true;
475
597
  }
476
598
 
@@ -810,8 +932,13 @@ function normalizeConfusables(text) {
810
932
  return text.normalize("NFKC").replace(CONFUSABLE_RE, (ch) => CONFUSABLE_MAP[ch] ?? ch);
811
933
  }
812
934
  var INJECTION_PATTERNS = [
813
- // Role hijacking
814
- { category: "role_hijack", pattern: /\b(?:you are|act as|pretend to be|roleplay as)\b/i },
935
+ // Role hijacking — agents legitimately describe themselves with "act as" /
936
+ // "you are" in their public cards, so this is noisy in structured contexts.
937
+ {
938
+ category: "role_hijack",
939
+ pattern: /\b(?:you are|act as|pretend to be|roleplay as)\b/i,
940
+ noisy: true
941
+ },
815
942
  // Instruction override
816
943
  {
817
944
  category: "instruction_override",
@@ -829,16 +956,35 @@ var INJECTION_PATTERNS = [
829
956
  },
830
957
  // Delimiter injection
831
958
  { category: "delimiter_injection", pattern: /<\/system>|\[\/INST\]|```system|<\|im_end\|>/i },
832
- // Data exfiltration
833
- { category: "data_exfil", pattern: /\b(?:send|post|leak).*?\b(?:secret|key|password)\b/i },
959
+ // Data exfiltration. Require a composite term ("secret key", "api key") or a strong
960
+ // single noun ("password", "credential", "seed phrase"). The previous version matched
961
+ // the bare word "key", which produces false positives on benign phrases like
962
+ // "send a link, get the key points" — common in legitimate agent descriptions.
963
+ // Cap the gap at 40 chars within the same clause (no sentence terminators) so the
964
+ // verb and noun must actually relate to each other. Composite-term separators are
965
+ // [\s_-]? so variants like `private-key`, `secret_key`, `seed-phrase` are caught.
966
+ // Marked `noisy` because even after tightening, NL verb+noun phrases remain a
967
+ // common source of FP on free-text agent descriptions.
968
+ {
969
+ category: "data_exfil",
970
+ pattern: /\b(?:send|post|exfiltrate|leak)\b[^.!?\n]{0,40}\b(?:secret[\s_-]?key|api[\s_-]?key|private[\s_-]?key|password|credential|auth[\s_-]?token|seed[\s_-]?phrase|mnemonic)\b/i,
971
+ noisy: true
972
+ },
834
973
  // Payment manipulation
835
974
  { category: "payment_manipulation", pattern: /\b(?:change|modify).*?\b(?:recipient|address)\b/i },
836
975
  { category: "payment_manipulation", pattern: /\bsend all funds\b/i },
837
- // Jailbreak
838
- { category: "jailbreak", pattern: /\b(?:DAN mode|developer mode enabled|from now on)\b/i },
839
- // Urgency
840
- { category: "urgency", pattern: /^(?:IMPORTANT|CRITICAL|URGENT|SYSTEM):/m }
976
+ // Jailbreak — "from now on" is a common phrase in changelogs/release notes that
977
+ // an agent could legitimately put in its description, hence noisy.
978
+ {
979
+ category: "jailbreak",
980
+ pattern: /\b(?:DAN mode|developer mode enabled|from now on)\b/i,
981
+ noisy: true
982
+ },
983
+ // Urgency — agents may put "IMPORTANT: rate limited to N req/s" in their card,
984
+ // so the line-anchored prefix is noisy in structured contexts.
985
+ { category: "urgency", pattern: /^(?:IMPORTANT|CRITICAL|URGENT|SYSTEM):/m, noisy: true }
841
986
  ];
987
+ var STRICT_INJECTION_PATTERNS = INJECTION_PATTERNS.filter((p) => !p.noisy);
842
988
  function stripDangerousUnicode(text) {
843
989
  return text.replace(
844
990
  // eslint-disable-next-line no-control-regex
@@ -852,14 +998,18 @@ function truncateLongLines(text) {
852
998
  ).join("\n");
853
999
  }
854
1000
  var INJECTION_SCAN_BUDGET = 8e3;
855
- function detectInjections(text) {
1001
+ function detectInjections(text, includeNoisy) {
856
1002
  const normalized = normalizeConfusables(text);
1003
+ const patterns = includeNoisy ? INJECTION_PATTERNS : STRICT_INJECTION_PATTERNS;
857
1004
  if (normalized.length <= INJECTION_SCAN_BUDGET) {
858
- return INJECTION_PATTERNS.some((p) => p.pattern.test(normalized));
1005
+ return patterns.some((p) => p.pattern.test(normalized));
859
1006
  }
860
1007
  const head = normalized.slice(0, INJECTION_SCAN_BUDGET);
861
1008
  const tail = normalized.slice(-INJECTION_SCAN_BUDGET);
862
- return INJECTION_PATTERNS.some((p) => p.pattern.test(head) || p.pattern.test(tail));
1009
+ return patterns.some((p) => p.pattern.test(head) || p.pattern.test(tail));
1010
+ }
1011
+ function scanForInjections(text, mode = "full") {
1012
+ return detectInjections(text, mode === "full");
863
1013
  }
864
1014
  function isLikelyBase64(s) {
865
1015
  if (s.length < 64) {
@@ -868,12 +1018,13 @@ function isLikelyBase64(s) {
868
1018
  const base64Chars = s.replace(/[A-Za-z0-9+/=\s]/g, "");
869
1019
  return base64Chars.length / s.length < 0.05;
870
1020
  }
871
- function sanitizeUntrusted(input, kind = "text") {
1021
+ function sanitizeUntrusted(input, kind = "text", options) {
872
1022
  let text = stripDangerousUnicode(input);
873
1023
  text = truncateLongLines(text);
874
1024
  text = text.replaceAll(BOUNDARY_BEGIN, "--- [UNTRUSTED MARKER STRIPPED] ---");
875
1025
  text = text.replaceAll(BOUNDARY_END, "--- [UNTRUSTED MARKER STRIPPED] ---");
876
- const injectionsDetected = kind !== "binary" && detectInjections(text);
1026
+ const scanned = kind !== "binary" && detectInjections(text, kind === "text");
1027
+ const injectionsDetected = scanned || options?.extraInjectionSignal === true;
877
1028
  let wrapped = `${BOUNDARY_BEGIN}
878
1029
  ${text}
879
1030
  ${BOUNDARY_END}`;
@@ -884,14 +1035,14 @@ ${wrapped}`;
884
1035
  }
885
1036
  return { text: wrapped, injectionsDetected };
886
1037
  }
1038
+ function sanitizeInner(input) {
1039
+ return truncateLongLines(stripDangerousUnicode(input));
1040
+ }
887
1041
  function sanitizeField(input, maxLen) {
888
1042
  let text = stripDangerousUnicode(input);
889
1043
  if (text.length > maxLen) {
890
1044
  text = text.slice(0, maxLen) + "...";
891
1045
  }
892
- if (detectInjections(text)) {
893
- text = `[SUSPICIOUS] ${text}`;
894
- }
895
1046
  return text;
896
1047
  }
897
1048
 
@@ -1212,13 +1363,26 @@ var customerTools = [
1212
1363
  console.error("[mcp:list_my_jobs] queryJobResults failed:", e);
1213
1364
  }
1214
1365
  }
1366
+ let freetextSuspicious = false;
1215
1367
  const results = mine.map((job) => {
1216
1368
  const dec = decryptedByRequest.get(job.eventId);
1217
1369
  let resultText;
1218
1370
  if (dec) {
1219
- resultText = dec.decryptionFailed ? "[decryption failed - targeted result not for this agent]" : sanitizeUntrusted(dec.content).text;
1371
+ if (dec.decryptionFailed) {
1372
+ resultText = "[decryption failed - targeted result not for this agent]";
1373
+ } else {
1374
+ const cleaned = sanitizeInner(dec.content);
1375
+ if (scanForInjections(cleaned, "full")) {
1376
+ freetextSuspicious = true;
1377
+ }
1378
+ resultText = cleaned;
1379
+ }
1220
1380
  } else if (job.result) {
1221
- resultText = sanitizeUntrusted(job.result).text;
1381
+ const cleaned = sanitizeInner(job.result);
1382
+ if (scanForInjections(cleaned, "full")) {
1383
+ freetextSuspicious = true;
1384
+ }
1385
+ resultText = cleaned;
1222
1386
  }
1223
1387
  return {
1224
1388
  event_id: job.eventId,
@@ -1229,10 +1393,11 @@ var customerTools = [
1229
1393
  result: resultText
1230
1394
  };
1231
1395
  });
1232
- return textResult(
1233
- `Found ${results.length} of your jobs:
1234
- ${JSON.stringify(results, null, 2)}`
1235
- );
1396
+ const { text: wrapped } = sanitizeUntrusted(JSON.stringify(results, null, 2), "structured", {
1397
+ extraInjectionSignal: freetextSuspicious
1398
+ });
1399
+ return textResult(`Found ${results.length} of your jobs:
1400
+ ${wrapped}`);
1236
1401
  }
1237
1402
  }),
1238
1403
  defineTool({
@@ -2378,6 +2543,9 @@ program.command("install").description("Install elisym MCP server into client co
2378
2543
  await runInstall({ client: options.client, agent: options.agent });
2379
2544
  }
2380
2545
  });
2546
+ program.command("update").description("Refresh the elisym MCP entry in installed client configs").option("--client <name>", "Specific client (claude-desktop, claude-code, cursor, windsurf)").option("--agent <name>", "Override the agent binding").action(async (options) => {
2547
+ await runUpdate({ client: options.client, agent: options.agent });
2548
+ });
2381
2549
  program.command("uninstall").description("Remove elisym from MCP client configs").option("--client <name>", "Specific client").action(async (options) => {
2382
2550
  await runUninstall({ client: options.client });
2383
2551
  });