@elisym/mcp 0.1.5 → 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
@@ -91,7 +91,7 @@ async function saveAgentConfig(name, config) {
91
91
  description: config.description
92
92
  },
93
93
  relays: config.relays,
94
- capabilities: config.capabilities,
94
+ capabilities: config.capabilities ?? [],
95
95
  ...config.solanaAddress && {
96
96
  payments: [
97
97
  {
@@ -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",
@@ -334,6 +363,15 @@ var CLIENTS = [
334
363
  }
335
364
  }
336
365
  },
366
+ {
367
+ name: "claude-code",
368
+ // Claude Code CLI keeps user-scope MCP servers under `mcpServers` at the top
369
+ // level of `~/.claude.json`. Project-scope (`.mcp.json` in cwd) and local-scope
370
+ // (`projects.<path>.mcpServers` inside the same file) are deliberately not
371
+ // touched here - this installer only writes user scope so the server is
372
+ // available across all projects.
373
+ configPath: () => join(homedir(), ".claude.json")
374
+ },
337
375
  {
338
376
  name: "cursor",
339
377
  configPath: () => join(homedir(), ".cursor/mcp.json")
@@ -352,7 +390,7 @@ var CLIENTS = [
352
390
  function buildServerEntry(agentName, env) {
353
391
  const entry = {
354
392
  command: "npx",
355
- args: ["-y", `@elisym/mcp@~${PACKAGE_VERSION}`]
393
+ args: elisymPackageArgs()
356
394
  };
357
395
  const mergedEnv = { ...env };
358
396
  if (agentName) {
@@ -364,6 +402,7 @@ function buildServerEntry(agentName, env) {
364
402
  return entry;
365
403
  }
366
404
  async function runInstall(options) {
405
+ validateClientName(options.client);
367
406
  if (options.agent) {
368
407
  validateAgentName(options.agent);
369
408
  }
@@ -393,7 +432,12 @@ async function runInstall(options) {
393
432
  console.log("No MCP clients found to install into.");
394
433
  }
395
434
  }
396
- 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;
397
441
  for (const client of CLIENTS) {
398
442
  if (options.client && client.name !== options.client) {
399
443
  continue;
@@ -402,15 +446,105 @@ async function runUninstall(options) {
402
446
  if (!path) {
403
447
  continue;
404
448
  }
449
+ let raw;
405
450
  try {
406
- const raw = await readFile(path, "utf-8");
407
- const config = JSON.parse(raw);
408
- if (config.mcpServers?.elisym) {
409
- delete config.mcpServers.elisym;
410
- await writeJsonAtomic(path, config);
411
- 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}`);
412
455
  }
456
+ continue;
457
+ }
458
+ let config;
459
+ try {
460
+ config = JSON.parse(raw);
413
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;
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);
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}`);
414
548
  }
415
549
  }
416
550
  }
@@ -435,18 +569,21 @@ async function writeJsonAtomic(path, data) {
435
569
  await writeFileAtomic(path, JSON.stringify(data, null, 2), 384);
436
570
  }
437
571
  async function installToConfig(path, entry) {
438
- let config;
439
572
  let raw;
440
573
  try {
441
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 {
442
584
  config = JSON.parse(raw);
443
585
  } catch {
444
- if (raw !== void 0) {
445
- console.error(
446
- `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.`
447
- );
448
- }
449
- config = {};
586
+ throw new Error(`${path} is not valid JSON. Fix the file manually and re-run install.`);
450
587
  }
451
588
  if (!config.mcpServers) {
452
589
  config.mcpServers = {};
@@ -454,14 +591,8 @@ async function installToConfig(path, entry) {
454
591
  if (config.mcpServers.elisym) {
455
592
  return false;
456
593
  }
457
- if (raw) {
458
- try {
459
- await writeFile(`${path}.elisym-backup`, raw, { mode: 384 });
460
- } catch {
461
- }
462
- }
463
594
  config.mcpServers.elisym = entry;
464
- await writeJsonAtomic(path, config);
595
+ await safeRewriteJson(path, raw, config);
465
596
  return true;
466
597
  }
467
598
 
@@ -480,7 +611,10 @@ function errorResult(text) {
480
611
  var CreateAgentSchema = z.object({
481
612
  name: z.string().min(1).max(64),
482
613
  description: z.string().default("Elisym MCP agent"),
483
- capabilities: z.string().default("mcp-gateway"),
614
+ // capabilities are intentionally not exposed: the MCP server runs in
615
+ // customer-mode in 0.1.x and never publishes a NIP-89 capability card,
616
+ // so an advertised capability list would be misleading. Provider-mode
617
+ // (0.2.0) will reintroduce this field.
484
618
  network: z.enum(["devnet", "mainnet"]).default("devnet"),
485
619
  passphrase: z.string().optional().describe("Optional passphrase; if set, secret keys are encrypted at rest."),
486
620
  activate: z.boolean().default(true)
@@ -564,11 +698,9 @@ var agentTools = [
564
698
  const solanaKeypair = Keypair.generate();
565
699
  const nostrSecretHex = Buffer.from(nostrSecretKey).toString("hex");
566
700
  const solanaSecretBase58 = bs58.encode(solanaKeypair.secretKey);
567
- const capabilities = input.capabilities.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map((tag) => ({ name: tag, description: tag, tags: [tag], price: 0 }));
568
701
  await saveAgentConfig(input.name, {
569
702
  name: input.name,
570
703
  description: input.description,
571
- capabilities,
572
704
  relays: [...RELAYS],
573
705
  nostrSecretKey: nostrSecretHex,
574
706
  solanaSecretKey: solanaSecretBase58,
@@ -800,8 +932,13 @@ function normalizeConfusables(text) {
800
932
  return text.normalize("NFKC").replace(CONFUSABLE_RE, (ch) => CONFUSABLE_MAP[ch] ?? ch);
801
933
  }
802
934
  var INJECTION_PATTERNS = [
803
- // Role hijacking
804
- { 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
+ },
805
942
  // Instruction override
806
943
  {
807
944
  category: "instruction_override",
@@ -819,16 +956,35 @@ var INJECTION_PATTERNS = [
819
956
  },
820
957
  // Delimiter injection
821
958
  { category: "delimiter_injection", pattern: /<\/system>|\[\/INST\]|```system|<\|im_end\|>/i },
822
- // Data exfiltration
823
- { 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
+ },
824
973
  // Payment manipulation
825
974
  { category: "payment_manipulation", pattern: /\b(?:change|modify).*?\b(?:recipient|address)\b/i },
826
975
  { category: "payment_manipulation", pattern: /\bsend all funds\b/i },
827
- // Jailbreak
828
- { category: "jailbreak", pattern: /\b(?:DAN mode|developer mode enabled|from now on)\b/i },
829
- // Urgency
830
- { 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 }
831
986
  ];
987
+ var STRICT_INJECTION_PATTERNS = INJECTION_PATTERNS.filter((p) => !p.noisy);
832
988
  function stripDangerousUnicode(text) {
833
989
  return text.replace(
834
990
  // eslint-disable-next-line no-control-regex
@@ -842,14 +998,18 @@ function truncateLongLines(text) {
842
998
  ).join("\n");
843
999
  }
844
1000
  var INJECTION_SCAN_BUDGET = 8e3;
845
- function detectInjections(text) {
1001
+ function detectInjections(text, includeNoisy) {
846
1002
  const normalized = normalizeConfusables(text);
1003
+ const patterns = includeNoisy ? INJECTION_PATTERNS : STRICT_INJECTION_PATTERNS;
847
1004
  if (normalized.length <= INJECTION_SCAN_BUDGET) {
848
- return INJECTION_PATTERNS.some((p) => p.pattern.test(normalized));
1005
+ return patterns.some((p) => p.pattern.test(normalized));
849
1006
  }
850
1007
  const head = normalized.slice(0, INJECTION_SCAN_BUDGET);
851
1008
  const tail = normalized.slice(-INJECTION_SCAN_BUDGET);
852
- 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");
853
1013
  }
854
1014
  function isLikelyBase64(s) {
855
1015
  if (s.length < 64) {
@@ -858,12 +1018,13 @@ function isLikelyBase64(s) {
858
1018
  const base64Chars = s.replace(/[A-Za-z0-9+/=\s]/g, "");
859
1019
  return base64Chars.length / s.length < 0.05;
860
1020
  }
861
- function sanitizeUntrusted(input, kind = "text") {
1021
+ function sanitizeUntrusted(input, kind = "text", options) {
862
1022
  let text = stripDangerousUnicode(input);
863
1023
  text = truncateLongLines(text);
864
1024
  text = text.replaceAll(BOUNDARY_BEGIN, "--- [UNTRUSTED MARKER STRIPPED] ---");
865
1025
  text = text.replaceAll(BOUNDARY_END, "--- [UNTRUSTED MARKER STRIPPED] ---");
866
- const injectionsDetected = kind !== "binary" && detectInjections(text);
1026
+ const scanned = kind !== "binary" && detectInjections(text, kind === "text");
1027
+ const injectionsDetected = scanned || options?.extraInjectionSignal === true;
867
1028
  let wrapped = `${BOUNDARY_BEGIN}
868
1029
  ${text}
869
1030
  ${BOUNDARY_END}`;
@@ -874,14 +1035,14 @@ ${wrapped}`;
874
1035
  }
875
1036
  return { text: wrapped, injectionsDetected };
876
1037
  }
1038
+ function sanitizeInner(input) {
1039
+ return truncateLongLines(stripDangerousUnicode(input));
1040
+ }
877
1041
  function sanitizeField(input, maxLen) {
878
1042
  let text = stripDangerousUnicode(input);
879
1043
  if (text.length > maxLen) {
880
1044
  text = text.slice(0, maxLen) + "...";
881
1045
  }
882
- if (detectInjections(text)) {
883
- text = `[SUSPICIOUS] ${text}`;
884
- }
885
1046
  return text;
886
1047
  }
887
1048
 
@@ -1202,13 +1363,26 @@ var customerTools = [
1202
1363
  console.error("[mcp:list_my_jobs] queryJobResults failed:", e);
1203
1364
  }
1204
1365
  }
1366
+ let freetextSuspicious = false;
1205
1367
  const results = mine.map((job) => {
1206
1368
  const dec = decryptedByRequest.get(job.eventId);
1207
1369
  let resultText;
1208
1370
  if (dec) {
1209
- 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
+ }
1210
1380
  } else if (job.result) {
1211
- resultText = sanitizeUntrusted(job.result).text;
1381
+ const cleaned = sanitizeInner(job.result);
1382
+ if (scanForInjections(cleaned, "full")) {
1383
+ freetextSuspicious = true;
1384
+ }
1385
+ resultText = cleaned;
1212
1386
  }
1213
1387
  return {
1214
1388
  event_id: job.eventId,
@@ -1219,10 +1393,11 @@ var customerTools = [
1219
1393
  result: resultText
1220
1394
  };
1221
1395
  });
1222
- return textResult(
1223
- `Found ${results.length} of your jobs:
1224
- ${JSON.stringify(results, null, 2)}`
1225
- );
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}`);
1226
1401
  }
1227
1402
  }),
1228
1403
  defineTool({
@@ -2292,7 +2467,7 @@ program.action(async () => {
2292
2467
  }
2293
2468
  await startServer(ctx);
2294
2469
  });
2295
- program.command("init [name]").description("Create a new agent identity").option("-d, --description <desc>", "Agent description", "Elisym MCP agent").option("-c, --capabilities <caps>", "Comma-separated capabilities", "mcp-gateway").option("-n, --network <network>", "Solana network (devnet|mainnet)", "devnet").option("--install", "Also install into MCP clients").action(async (name, options) => {
2470
+ program.command("init [name]").description("Create a new agent identity").option("-d, --description <desc>", "Agent description", "Elisym MCP agent").option("-n, --network <network>", "Solana network (devnet|mainnet)", "devnet").option("--install", "Also install into MCP clients").action(async (name, options) => {
2296
2471
  const { default: inquirer } = await import('inquirer');
2297
2472
  if (!name) {
2298
2473
  const answers = await inquirer.prompt([
@@ -2308,12 +2483,6 @@ program.command("init [name]").description("Create a new agent identity").option
2308
2483
  message: "Description:",
2309
2484
  default: "Elisym MCP agent"
2310
2485
  },
2311
- {
2312
- type: "input",
2313
- name: "capabilities",
2314
- message: "Capabilities (comma-separated):",
2315
- default: "mcp-gateway"
2316
- },
2317
2486
  {
2318
2487
  type: "list",
2319
2488
  name: "network",
@@ -2325,7 +2494,6 @@ program.command("init [name]").description("Create a new agent identity").option
2325
2494
  ]);
2326
2495
  name = answers.name;
2327
2496
  options.description = answers.description;
2328
- options.capabilities = answers.capabilities;
2329
2497
  options.network = answers.network;
2330
2498
  }
2331
2499
  if (options.network !== "devnet" && options.network !== "mainnet") {
@@ -2343,11 +2511,9 @@ program.command("init [name]").description("Create a new agent identity").option
2343
2511
  const nostrSecretKey = generateSecretKey();
2344
2512
  const nostrPubkey = getPublicKey(nostrSecretKey);
2345
2513
  const solanaKeypair = Keypair.generate();
2346
- const capabilities = options.capabilities.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map((tag) => ({ name: tag, description: tag, tags: [tag], price: 0 }));
2347
2514
  await saveAgentConfig(name, {
2348
2515
  name,
2349
2516
  description: options.description,
2350
- capabilities,
2351
2517
  relays: [...RELAYS],
2352
2518
  nostrSecretKey: Buffer.from(nostrSecretKey).toString("hex"),
2353
2519
  solanaSecretKey: bs58.encode(solanaKeypair.secretKey),
@@ -2370,13 +2536,16 @@ program.command("init [name]").description("Create a new agent identity").option
2370
2536
  await runInstall({ agent: name });
2371
2537
  }
2372
2538
  });
2373
- program.command("install").description("Install elisym MCP server into client configs").option("--client <name>", "Specific client (claude-desktop, cursor, windsurf)").option("--agent <name>", "Bind to specific agent").option("--list", "List detected clients").action(async (options) => {
2539
+ program.command("install").description("Install elisym MCP server into client configs").option("--client <name>", "Specific client (claude-desktop, claude-code, cursor, windsurf)").option("--agent <name>", "Bind to specific agent").option("--list", "List detected clients").action(async (options) => {
2374
2540
  if (options.list) {
2375
2541
  await runList();
2376
2542
  } else {
2377
2543
  await runInstall({ client: options.client, agent: options.agent });
2378
2544
  }
2379
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
+ });
2380
2549
  program.command("uninstall").description("Remove elisym from MCP client configs").option("--client <name>", "Specific client").action(async (options) => {
2381
2550
  await runUninstall({ client: options.client });
2382
2551
  });