@elisym/mcp 0.3.1 → 0.3.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.
package/README.md CHANGED
@@ -106,15 +106,15 @@ The bootstrap step is unchanged - the wizard collects the passphrase interactive
106
106
 
107
107
  ## Environment Variables
108
108
 
109
- | Variable | Description |
110
- | --------------------------- | ----------------------------------------------------------------------------- |
111
- | `ELISYM_AGENT` | Load agent from `~/.elisym/agents/<name>/` |
112
- | `ELISYM_NOSTR_SECRET` | Nostr secret key (hex or nsec) for ephemeral mode |
113
- | `ELISYM_AGENT_NAME` | Agent display name (default: mcp-agent) |
114
- | `ELISYM_NETWORK` | Solana network for ephemeral mode: `devnet` or `mainnet` (default: devnet) |
115
- | `ELISYM_PASSPHRASE` | Passphrase for encrypted agent configs (optional) |
116
- | `ELISYM_ALLOW_WITHDRAWAL` | Set to `1` to override per-agent `security.withdrawals_enabled` flag (CI use) |
117
- | `ELISYM_ALLOW_AGENT_SWITCH` | Set to `1` to override per-agent `security.agent_switch_enabled` flag |
109
+ | Variable | Description |
110
+ | --------------------------- | ------------------------------------------------------------------------------- |
111
+ | `ELISYM_AGENT` | Load agent from `~/.elisym/agents/<name>/` |
112
+ | `ELISYM_NOSTR_SECRET` | Nostr secret key (hex or nsec) for ephemeral mode |
113
+ | `ELISYM_AGENT_NAME` | Agent display name (default: mcp-agent) |
114
+ | `ELISYM_NETWORK` | Solana network for ephemeral mode. Only `devnet` is supported (default: devnet) |
115
+ | `ELISYM_PASSPHRASE` | Passphrase for encrypted agent configs (optional) |
116
+ | `ELISYM_ALLOW_WITHDRAWAL` | Set to `1` to override per-agent `security.withdrawals_enabled` flag (CI use) |
117
+ | `ELISYM_ALLOW_AGENT_SWITCH` | Set to `1` to override per-agent `security.agent_switch_enabled` flag |
118
118
 
119
119
  ## Usage Examples
120
120
 
package/dist/index.js CHANGED
@@ -42,8 +42,16 @@ function agentsDir() {
42
42
  function agentConfigPath(name) {
43
43
  return join(agentsDir(), name, "config.json");
44
44
  }
45
- function coerceNetwork(raw) {
46
- return raw === "mainnet" ? "mainnet" : "devnet";
45
+ function coerceNetwork(raw, agentName) {
46
+ if (raw === void 0 || raw === "devnet") {
47
+ return "devnet";
48
+ }
49
+ if (raw === "mainnet") {
50
+ throw new Error(
51
+ `Agent "${agentName}" is configured for mainnet, which is not supported until the elisym-config program is deployed there. Re-create the agent with --network devnet: rm -rf ~/.elisym/agents/${agentName} && elisym-mcp init ${agentName} --network devnet`
52
+ );
53
+ }
54
+ throw new Error(`Agent "${agentName}" has unsupported network "${raw}". Expected "devnet".`);
47
55
  }
48
56
  async function loadAgentConfig(name, passphrase) {
49
57
  validateAgentName(name);
@@ -65,7 +73,7 @@ async function loadAgentConfig(name, passphrase) {
65
73
  `Failed to load agent "${name}": ${msg}. If this config was created by an earlier version, delete ~/.elisym/agents/${name} and run "elisym-mcp init ${name}" again.`
66
74
  );
67
75
  }
68
- const network = coerceNetwork(config.wallet?.network ?? config.payments?.[0]?.network);
76
+ const network = coerceNetwork(config.wallet?.network ?? config.payments?.[0]?.network, name);
69
77
  return {
70
78
  nostrSecretKey: config.identity.secret_key,
71
79
  solanaSecretKey: config.wallet?.secret_key,
@@ -160,18 +168,17 @@ async function listAgentNames() {
160
168
  return [];
161
169
  }
162
170
  }
163
- function rpcUrlFor(network) {
164
- return network === "mainnet" ? "https://api.mainnet-beta.solana.com" : "https://api.devnet.solana.com";
171
+ function rpcUrlFor(_network) {
172
+ return "https://api.devnet.solana.com";
165
173
  }
166
- async function fetchProtocolConfig(network) {
167
- const cluster = network === "mainnet" ? "mainnet" : "devnet";
168
- const programId = getProtocolProgramId(cluster);
169
- const rpc = createSolanaRpc(rpcUrlFor(cluster));
174
+ async function fetchProtocolConfig(_network) {
175
+ const programId = getProtocolProgramId("devnet");
176
+ const rpc = createSolanaRpc(rpcUrlFor());
170
177
  const config = await getProtocolConfig(rpc, programId, { forceRefresh: true });
171
178
  return { feeBps: config.feeBps, treasury: config.treasury };
172
179
  }
173
- function explorerClusterFor(network) {
174
- return network === "mainnet" ? "mainnet-beta" : "devnet";
180
+ function explorerClusterFor(_network) {
181
+ return "devnet";
175
182
  }
176
183
  var RateLimiter = class {
177
184
  constructor(maxCalls, windowSecs) {
@@ -632,7 +639,10 @@ var CreateAgentSchema = z.object({
632
639
  // customer-mode in 0.1.x and never publishes a NIP-89 capability card,
633
640
  // so an advertised capability list would be misleading. Provider-mode
634
641
  // (0.2.0) will reintroduce this field.
635
- network: z.enum(["devnet", "mainnet"]).default("devnet"),
642
+ // Only devnet is supported until the elisym-config program ships on mainnet.
643
+ // Mainnet was previously advertised here but every paid flow then crashed at
644
+ // payment time - rejecting at schema level surfaces the error up front.
645
+ network: z.enum(["devnet"]).default("devnet"),
636
646
  passphrase: z.string().optional().describe("Optional passphrase; if set, secret keys are encrypted at rest."),
637
647
  activate: z.boolean().default(true)
638
648
  });
@@ -1072,6 +1082,7 @@ function sanitizeField(input, maxLen) {
1072
1082
  }
1073
1083
 
1074
1084
  // src/tools/customer.ts
1085
+ var PRE_PING_TIMEOUT_MS = 5e3;
1075
1086
  var CreateJobSchema = z.object({
1076
1087
  input: z.string().describe("The job prompt/input sent to the provider."),
1077
1088
  capability: z.string().min(1).max(64).default("general").describe("Short tag selecting which capability of the provider to invoke."),
@@ -1192,18 +1203,34 @@ function makePaymentFeedbackHandler(opts) {
1192
1203
  );
1193
1204
  return;
1194
1205
  }
1195
- if (opts.maxPriceLamports === void 0 && amount !== void 0 && amount > 0) {
1206
+ let parsedRequest;
1207
+ try {
1208
+ parsedRequest = JSON.parse(paymentRequest);
1209
+ } catch {
1210
+ opts.rejectPayment(new Error("Provider sent a malformed payment_request (not valid JSON)."));
1211
+ return;
1212
+ }
1213
+ const signedAmount = typeof parsedRequest.amount === "number" && Number.isInteger(parsedRequest.amount) && parsedRequest.amount > 0 ? parsedRequest.amount : void 0;
1214
+ if (amount !== void 0 && amount > 0 && signedAmount !== void 0 && amount !== signedAmount) {
1196
1215
  opts.rejectPayment(
1197
1216
  new Error(
1198
- `Payment of ${formatSol(BigInt(amount))} required but no max_price_lamports set. Retry with max_price_lamports to approve.`
1217
+ `Payment request mismatch: feedback tag amount=${amount} differs from signed amount=${signedAmount}. Refusing to proceed.`
1199
1218
  )
1200
1219
  );
1201
1220
  return;
1202
1221
  }
1203
- if (opts.maxPriceLamports !== void 0 && amount !== void 0 && amount > opts.maxPriceLamports) {
1222
+ if (opts.maxPriceLamports === void 0 && signedAmount !== void 0) {
1204
1223
  opts.rejectPayment(
1205
1224
  new Error(
1206
- `Price ${formatSol(BigInt(amount))} exceeds max ${formatSol(BigInt(opts.maxPriceLamports))}`
1225
+ `Payment of ${formatSol(BigInt(signedAmount))} required but no max_price_lamports set. Retry with max_price_lamports to approve.`
1226
+ )
1227
+ );
1228
+ return;
1229
+ }
1230
+ if (opts.maxPriceLamports !== void 0 && signedAmount !== void 0 && signedAmount > opts.maxPriceLamports) {
1231
+ opts.rejectPayment(
1232
+ new Error(
1233
+ `Price ${formatSol(BigInt(signedAmount))} exceeds max ${formatSol(BigInt(opts.maxPriceLamports))}`
1207
1234
  )
1208
1235
  );
1209
1236
  return;
@@ -1211,7 +1238,7 @@ function makePaymentFeedbackHandler(opts) {
1211
1238
  if (!opts.agent.solanaKeypair) {
1212
1239
  opts.resolveNoWallet(
1213
1240
  `Payment required but no Solana wallet configured.
1214
- Amount: ${amount ? formatSol(BigInt(amount)) : "unknown"}
1241
+ Amount: ${signedAmount !== void 0 ? formatSol(BigInt(signedAmount)) : "unknown"}
1215
1242
  Payment request: ${paymentRequest}`
1216
1243
  );
1217
1244
  return;
@@ -1445,6 +1472,12 @@ ${wrapped}`);
1445
1472
  const timeout = Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3;
1446
1473
  const agent = ctx.active();
1447
1474
  const providerPubkey = decodeNpub(input.provider_npub);
1475
+ const ping = await agent.client.ping.pingAgent(providerPubkey, PRE_PING_TIMEOUT_MS);
1476
+ if (!ping.online) {
1477
+ return errorResult(
1478
+ `Provider ${input.provider_npub} is offline. Run search_agents to find currently-online providers.`
1479
+ );
1480
+ }
1448
1481
  const providers = await agent.client.discovery.fetchAgents(agent.network);
1449
1482
  const provider = providers.find((a) => a.npub === input.provider_npub);
1450
1483
  if (!provider) {
@@ -1526,6 +1559,12 @@ ${result}`);
1526
1559
  const agent = ctx.active();
1527
1560
  const providerPubkey = decodeNpub(input.provider_npub);
1528
1561
  const dTag = toDTag(input.capability);
1562
+ const ping = await agent.client.ping.pingAgent(providerPubkey, PRE_PING_TIMEOUT_MS);
1563
+ if (!ping.online) {
1564
+ return errorResult(
1565
+ `Provider ${input.provider_npub} is offline. Run search_agents to find currently-online providers.`
1566
+ );
1567
+ }
1529
1568
  const agents = await agent.client.discovery.fetchAgents(agent.network);
1530
1569
  const provider = agents.find((a) => a.npub === input.provider_npub);
1531
1570
  if (!provider) {
@@ -1629,7 +1668,7 @@ ${result}`);
1629
1668
  var GetDashboardSchema = z.object({
1630
1669
  top_n: z.number().int().min(1).max(100).default(10),
1631
1670
  chain: z.enum(["solana"]).default("solana"),
1632
- network: z.enum(["devnet", "mainnet"]).optional(),
1671
+ network: z.enum(["devnet"]).optional(),
1633
1672
  timeout_secs: z.number().int().min(1).max(60).default(15)
1634
1673
  });
1635
1674
  var dashboardTools = [
@@ -1667,6 +1706,8 @@ ${text}`);
1667
1706
  }
1668
1707
  })
1669
1708
  ];
1709
+ var LAST_SEEN_ONLINE_WINDOW_SECS = 10 * 60;
1710
+ var SEARCH_PING_TIMEOUT_MS = 3e3;
1670
1711
  var STOP_WORDS = /* @__PURE__ */ new Set([
1671
1712
  "a",
1672
1713
  "an",
@@ -1800,28 +1841,19 @@ var SearchAgentsSchema = z.object({
1800
1841
  capabilities: z.array(z.string()).min(1).describe("OR-matched substring filter on agent names, descriptions, and capability tags."),
1801
1842
  query: z.string().optional().describe("Optional secondary scoring for re-ranking. Omit when you have precise tokens."),
1802
1843
  max_price_lamports: z.number().int().optional(),
1803
- // rename in description so it's obvious we're using a heuristic freshness signal,
1804
- // not a live reachability probe.
1805
- recently_active_only: z.boolean().default(true).describe(
1806
- "If true, only return agents with job activity in the last hour. Not a liveness probe."
1844
+ include_offline: z.boolean().default(false).describe(
1845
+ "If true, skip the live online check and return agents regardless of reachability. Default: false - only currently-online agents are returned."
1807
1846
  )
1808
1847
  });
1809
1848
  var ListCapabilitiesSchema = z.object({});
1810
1849
  var GetIdentitySchema = z.object({});
1811
- var PingAgentSchema = z.object({
1812
- agent_npub: z.string(),
1813
- // single source of truth for the default.
1814
- timeout_secs: z.number().int().min(1).max(600).default(15)
1815
- });
1816
1850
  var discoveryTools = [
1817
1851
  defineTool({
1818
1852
  name: "search_agents",
1819
- // previous description was ~90 tokens and was truncated by some MCP clients.
1820
- // Keep the operational rules short; schema `.describe()` fields carry the detail.
1821
- description: "Search AI agents. `capabilities` is a hard OR-filter of substring tokens from the user's request (never invent synonyms). `query` is optional re-ranking; omit if not needed.",
1853
+ description: "Search AI agents currently online on elisym. `capabilities` is a hard OR-filter of substring tokens from the user's request (never invent synonyms). `query` is optional re-ranking; omit if not needed. Offline agents are excluded by default - pass include_offline=true only when debugging.",
1822
1854
  schema: SearchAgentsSchema,
1823
1855
  async handler(ctx, input) {
1824
- const { capabilities, query, max_price_lamports, recently_active_only } = input;
1856
+ const { capabilities, query, max_price_lamports, include_offline } = input;
1825
1857
  if (capabilities.length > MAX_CAPABILITIES) {
1826
1858
  return errorResult(`Too many capabilities (max ${MAX_CAPABILITIES})`);
1827
1859
  }
@@ -1834,10 +1866,6 @@ var discoveryTools = [
1834
1866
  )
1835
1867
  )
1836
1868
  );
1837
- if (recently_active_only) {
1838
- const activeThreshold = Math.floor(Date.now() / 1e3) - 60 * 60;
1839
- filtered = filtered.filter((a) => a.lastSeen >= activeThreshold);
1840
- }
1841
1869
  if (max_price_lamports !== void 0) {
1842
1870
  filtered = filtered.filter(
1843
1871
  (a) => a.cards.some(
@@ -1845,6 +1873,25 @@ var discoveryTools = [
1845
1873
  )
1846
1874
  );
1847
1875
  }
1876
+ if (!include_offline) {
1877
+ const freshThreshold = Math.floor(Date.now() / 1e3) - LAST_SEEN_ONLINE_WINDOW_SECS;
1878
+ filtered = filtered.filter((a) => a.lastSeen >= freshThreshold);
1879
+ if (filtered.length > 0) {
1880
+ const probes = await Promise.allSettled(
1881
+ filtered.map(
1882
+ (candidate) => agent.client.ping.pingAgent(candidate.pubkey, SEARCH_PING_TIMEOUT_MS)
1883
+ )
1884
+ );
1885
+ const survivors = [];
1886
+ filtered.forEach((candidate, index) => {
1887
+ const probe = probes[index];
1888
+ if (probe?.status === "fulfilled" && probe.value.online) {
1889
+ survivors.push(candidate);
1890
+ }
1891
+ });
1892
+ filtered = survivors;
1893
+ }
1894
+ }
1848
1895
  if (query) {
1849
1896
  const isAscii = /^[\u0000-\u007F]*$/.test(query);
1850
1897
  const words = query.toLowerCase().split(/\s+/).filter((w) => w.length > 1 && (!isAscii || !STOP_WORDS.has(w)));
@@ -1865,7 +1912,7 @@ var discoveryTools = [
1865
1912
  }
1866
1913
  if (filtered.length === 0) {
1867
1914
  return textResult(
1868
- recently_active_only ? "No recently-active agents found matching those capabilities. Try with recently_active_only=false." : "No agents found matching those capabilities."
1915
+ include_offline ? "No agents found matching those capabilities." : "No online agents found matching those capabilities. Retry shortly or pass include_offline=true to see unreachable matches."
1869
1916
  );
1870
1917
  }
1871
1918
  const results = filtered.map((a) => ({
@@ -1927,31 +1974,6 @@ ${text}`);
1927
1974
  )
1928
1975
  );
1929
1976
  }
1930
- }),
1931
- defineTool({
1932
- name: "ping_agent",
1933
- description: "Ping an agent to check if it's online. Sends an encrypted heartbeat and waits for a pong.",
1934
- schema: PingAgentSchema,
1935
- async handler(ctx, input) {
1936
- ctx.toolRateLimiter.check();
1937
- const { agent_npub, timeout_secs } = input;
1938
- const agent = ctx.active();
1939
- let pubkey;
1940
- try {
1941
- const decoded = nip19.decode(agent_npub);
1942
- if (decoded.type !== "npub") {
1943
- return errorResult(`Expected npub, got ${decoded.type}`);
1944
- }
1945
- pubkey = decoded.data;
1946
- } catch {
1947
- return errorResult(`Invalid npub: ${agent_npub}`);
1948
- }
1949
- const timeoutMs = timeout_secs * 1e3;
1950
- const result = await agent.client.ping.pingAgent(pubkey, timeoutMs);
1951
- return textResult(
1952
- result.online ? `Agent ${agent_npub} is online.` : `Agent ${agent_npub} did not respond within ${timeout_secs}s.`
1953
- );
1954
- }
1955
1977
  })
1956
1978
  ];
1957
1979
  var GetBalanceSchema = z.object({});
@@ -2415,9 +2437,14 @@ program.action(async () => {
2415
2437
  }
2416
2438
  const client = new ElisymClient({ relays: RELAYS });
2417
2439
  const name = process.env.ELISYM_AGENT_NAME ?? "mcp-agent";
2418
- const network = process.env.ELISYM_NETWORK === "mainnet" ? "mainnet" : "devnet";
2419
- ctx.register({ client, identity, name, network, security: {} });
2420
- console.error(`Ephemeral agent: ${name} (${network})`);
2440
+ if (process.env.ELISYM_NETWORK && process.env.ELISYM_NETWORK !== "devnet") {
2441
+ console.error(
2442
+ `ELISYM_NETWORK="${process.env.ELISYM_NETWORK}" is not supported. Only "devnet" is available until the on-chain protocol program ships on mainnet.`
2443
+ );
2444
+ process.exit(1);
2445
+ }
2446
+ ctx.register({ client, identity, name, network: "devnet", security: {} });
2447
+ console.error(`Ephemeral agent: ${name} (devnet)`);
2421
2448
  } else {
2422
2449
  const names = (await listAgentNames()).slice().sort();
2423
2450
  if (names.length > 0) {
@@ -2440,7 +2467,7 @@ program.action(async () => {
2440
2467
  }
2441
2468
  await startServer(ctx);
2442
2469
  });
2443
- 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) => {
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 only)", "devnet").option("--install", "Also install into MCP clients").action(async (name, options) => {
2444
2471
  const { default: inquirer } = await import('inquirer');
2445
2472
  if (!name) {
2446
2473
  const answers = await inquirer.prompt([
@@ -2460,8 +2487,8 @@ program.command("init [name]").description("Create a new agent identity").option
2460
2487
  type: "list",
2461
2488
  name: "network",
2462
2489
  message: "Solana network:",
2463
- // testnet removed - only devnet and mainnet are supported.
2464
- choices: ["devnet", "mainnet"],
2490
+ // Only devnet is supported until the elisym-config program ships on mainnet.
2491
+ choices: ["devnet"],
2465
2492
  default: "devnet"
2466
2493
  }
2467
2494
  ]);
@@ -2469,8 +2496,10 @@ program.command("init [name]").description("Create a new agent identity").option
2469
2496
  options.description = answers.description;
2470
2497
  options.network = answers.network;
2471
2498
  }
2472
- if (options.network !== "devnet" && options.network !== "mainnet") {
2473
- console.error(`Network must be "devnet" or "mainnet", got "${options.network}".`);
2499
+ if (options.network !== "devnet") {
2500
+ console.error(
2501
+ `Network must be "devnet", got "${options.network}". Mainnet is not supported until the on-chain protocol program is deployed.`
2502
+ );
2474
2503
  process.exit(1);
2475
2504
  }
2476
2505
  const { passphrase } = await inquirer.prompt([
@@ -2492,7 +2521,7 @@ program.command("init [name]").description("Create a new agent identity").option
2492
2521
  nostrSecretKey: Buffer.from(nostrSecretKey).toString("hex"),
2493
2522
  solanaSecretKey: bs58.encode(solanaSecretBytes),
2494
2523
  solanaAddress: solanaSigner.address,
2495
- network: options.network,
2524
+ network: "devnet",
2496
2525
  security: { withdrawals_enabled: false, agent_switch_enabled: false },
2497
2526
  passphrase: passphrase || void 0
2498
2527
  });