@fidesbuild/mcp-server 0.10.0 → 0.11.1

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/server.js +41 -7
  2. package/package.json +2 -2
package/dist/server.js CHANGED
@@ -16,7 +16,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
16
16
  import { z } from 'zod';
17
17
  import { encodeFunctionData, decodeFunctionResult, erc20Abi, formatUnits, parseUnits, isAddress, getAddress } from 'viem';
18
18
  import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
19
- import { Fides, FidesBuilder, Policy, Receipts, accountOf, createFidesClient, fromJson, toJson } from '@fidesbuild/sdk';
19
+ import { Fides, FidesBuilder, Policy, Receipts, RelayFees, accountOf, createFidesClient, fromJson, toJson } from '@fidesbuild/sdk';
20
20
  import { createX402, decodePaymentRequired, exactEip3009 } from '@fidesbuild/x402';
21
21
  import { netObligations, settle, netRoots, PUBLISH_SELECTOR, NetError } from '@fidesbuild/net';
22
22
  import { createSubClient, SubFidesKit, SubAgent, SubFidesError, subDeployments } from '@fidesbuild/sub';
@@ -251,7 +251,7 @@ export function createFidesMcpServer(cfg) {
251
251
  ...(outside > 0n ? { warning: `${amount(outside)} sits on the agent key ${agent.address} outside the FIDES: it was sent to the agent address directly instead of to the FIDES Account. FIDES never spends it. To get it back, the owner can import the agent key from ${path.join(cfg.home, 'agent.json')} into a wallet on this computer, add a little ETH for gas, and send it to themselves. Funds for the agent always go to the FIDES Account address.` } : {}),
252
252
  };
253
253
  }
254
- const server = new McpServer({ name: 'fides', version: '0.10.0' }, {
254
+ const server = new McpServer({ name: 'fides', version: '0.11.0' }, {
255
255
  instructions: 'FIDES lets you act with the owner\'s funds only inside limits the owner signed (a FIDES): a budget, allowed contracts and functions, paid services, and an expiry. ' +
256
256
  'Funds move from the owner\'s FIDES Account, never from the owner\'s wallet. If no FIDES is set up yet, call fides_setup and give the user the link. Always call fides_plan first and show the user what is inside the FIDES, what is blocked and why, ' +
257
257
  'and ask before calling fides_execute. Actions outside the FIDES are blocked before anything is sent. Amounts are in whole token units, like "1.5". ' +
@@ -279,6 +279,7 @@ export function createFidesMcpServer(cfg) {
279
279
  'Buy from paid APIs that charge per request over x402, paying from the FIDES budget, each capped per service.',
280
280
  'Swap on Uniswap inside a trading mandate the owner signed: tokens it may buy, worst price, most per swap; best route across Robinhood Chain including v4 hook pools, approval and swap as one transaction (fides_swap). The FIDES must allow swaps: fides_setup with swap.',
281
281
  'Hand another agent a slice of the FIDES with its own budget and expiry (fides_delegate, SUB-FIDES); or, as the child, act inside a slice handed to you (fides_sub_plan). The FIDES must allow delegation: fides_setup with delegate: true.',
282
+ 'Keep running when the hosted relayer\'s free daily budget is used up, by paying the relay fee in $FIDES from the FIDES Account (fides_setup with relay_fees). Fees are only paid then, and fides_execute says when one was.',
282
283
  'Keep going for weeks on one signature (LONG FIDES): a budget per hour, day, week or month that resets by itself, a total it can never pass and an end date. fides_setup with keeps_going; then fides_plan, fides_swap and fides_pay_request work inside this period\'s budget, and fides_status shows what is left this period and when it resets.',
283
284
  'Settle with FIDES NET: give the payments owed to and from the FIDES Account, and they are netted to final positions and settled in one transaction, with the round published onchain (fides_net). The FIDES must allow NET: fides_setup with net: true.',
284
285
  'Show the budget spent and left, what is allowed, the expiry, and every transaction executed, with explorer links.',
@@ -332,13 +333,16 @@ export function createFidesMcpServer(cfg) {
332
333
  max_slippage_percent: z.number().positive().max(50).optional().describe('The worst price it may accept against the quote, in percent. Default 1'),
333
334
  max_per_swap: z.string().optional().describe('The most one swap may spend, in the budget token. Default "1"'),
334
335
  }).optional().describe('Let the agent swap on Uniswap inside a trading mandate the owner signs; fides_swap needs it'),
336
+ relay_fees: z.string().optional().describe('The most the agent may spend on relay fees in $FIDES, e.g. "100". Fees are only paid when the hosted relayer\'s free daily budget is used up, so the agent keeps running then. The owner funds the FIDES Account with that much $FIDES.'),
335
337
  keeps_going: z.object({
336
338
  every: z.enum(['hour', 'day', 'week', 'month']).describe('How often the budget resets'),
337
339
  per_period: z.string().describe('The most it may spend each period, in USDG, e.g. "5"'),
338
340
  days: z.number().positive().max(365).optional().describe('How long it keeps going, in days. Default 30'),
339
341
  }).optional().describe('A LONG FIDES: one signature, and the agent keeps going period after period, with a budget per period that resets by itself, a total it can never pass and an end date. It cannot be combined with delegate or net.'),
340
342
  },
341
- }, async ({ services, net, delegate, swap, contracts, keeps_going }) => {
343
+ }, async ({ services, net, delegate, swap, contracts, keeps_going, relay_fees }) => {
344
+ if (relay_fees !== undefined && !(Number(relay_fees) > 0))
345
+ return fail('relay_fees must be a number of $FIDES, like "100".');
342
346
  if (keeps_going && (delegate || net))
343
347
  return fail('A LONG FIDES cannot hand out slices or settle with NET. Leave out delegate and net, or sign an ordinary FIDES for those.');
344
348
  let targets = '';
@@ -372,7 +376,8 @@ export function createFidesMcpServer(cfg) {
372
376
  }
373
377
  return ok({
374
378
  agent_address: agent.address,
375
- link: setupLink + targets + (services ?? []).map((x) => `&service=${encodeURIComponent(x.id)}:${encodeURIComponent(x.max)}`).join('') + (net && netRoots[cfg.chainId] ? `&target=${netRoots[cfg.chainId]}:${PUBLISH_SELECTOR}` : '') + (delegate ? '&delegate=1' : '') + (swap ? `&swap=${encodeURIComponent(swap.buy.map((t) => (/^weth$/i.test(t) ? uniswap[cfg.chainId]?.weth ?? t : t)).join('|'))}:${Math.round((swap.max_slippage_percent ?? 1) * 100)}:${encodeURIComponent(swap.max_per_swap ?? '1')}` : '') + (keeps_going ? `&every=${keeps_going.every}&per=${encodeURIComponent(keeps_going.per_period)}&days=${keeps_going.days ?? 30}` : ''),
379
+ link: setupLink + targets + (services ?? []).map((x) => `&service=${encodeURIComponent(x.id)}:${encodeURIComponent(x.max)}`).join('') + (net && netRoots[cfg.chainId] ? `&target=${netRoots[cfg.chainId]}:${PUBLISH_SELECTOR}` : '') + (delegate ? '&delegate=1' : '') + (swap ? `&swap=${encodeURIComponent(swap.buy.map((t) => (/^weth$/i.test(t) ? uniswap[cfg.chainId]?.weth ?? t : t)).join('|'))}:${Math.round((swap.max_slippage_percent ?? 1) * 100)}:${encodeURIComponent(swap.max_per_swap ?? '1')}` : '') + (keeps_going ? `&every=${keeps_going.every}&per=${encodeURIComponent(keeps_going.per_period)}&days=${keeps_going.days ?? 30}` : '') + (relay_fees ? `&fee=${encodeURIComponent(relay_fees)}` : ''),
380
+ relay_fees: relay_fees ? `up to ${relay_fees} $FIDES on relay fees, paid only when the hosted relayer's free daily budget is used up. Fund the FIDES Account with that much $FIDES.` : undefined,
376
381
  keeps_going: keeps_going ? `${keeps_going.per_period} USDG every ${keeps_going.every}, for ${keeps_going.days ?? 30} days. The budget resets by itself; the total is the most it can ever spend.` : undefined,
377
382
  services: services?.length ? services.map((x) => `${x.id}: up to ${x.max} USDG`) : undefined,
378
383
  contracts: added.length ? added : undefined,
@@ -499,7 +504,7 @@ export function createFidesMcpServer(cfg) {
499
504
  const sim = p.bundle ? await la.simulate(p) : { success: false, reason: 'NOTHING_TO_SEND' };
500
505
  const id = (p.bundleHash ?? '0x').slice(0, 18);
501
506
  if (sim.success)
502
- longPlans.set(id, { agent: la, plan: p, fides });
507
+ longPlans.set(id, { agent: la, plan: p, fides, actions: built });
503
508
  return ok({
504
509
  plan_id: sim.success ? id : null,
505
510
  ready_to_execute: sim.success,
@@ -616,9 +621,32 @@ export function createFidesMcpServer(cfg) {
616
621
  return json;
617
622
  }
618
623
  /** Sends a plan's bundles: through the hosted relayer, or paid by the agent key with FIDES_RELAY=self. */
624
+ // The hosted relayer sends one bundle a day per FIDES Account for free. Past it, a bundle that carries the relay fee in $FIDES
625
+ // is still relayed, if the FIDES allows paying it. What was paid is kept for the tool's answer.
626
+ let lastRelayFee = null;
627
+ async function withRelayFee(fides, actions, bundles) {
628
+ if (!cfg.api)
629
+ return null;
630
+ const fees = await RelayFees.get(cfg.api, fides.account).catch(() => null);
631
+ if (!fees || !RelayFees.needed(fees, Math.max(1, bundles)))
632
+ return null;
633
+ if (!RelayFees.allows(fides, fees))
634
+ return 'This FIDES Account has used its free relayed bundle for today, and this FIDES does not allow paying the relay fee in $FIDES. Try again after 00:00 UTC, or the user signs a FIDES that allows it (fides_setup with relay_fees).';
635
+ const fee = RelayFees.action(fees, Math.max(1, bundles));
636
+ lastRelayFee = fee.label ?? null;
637
+ return [...actions.filter((a) => a.id !== 'relay-fee'), fee];
638
+ }
619
639
  async function send(plan, actions, fides) {
640
+ lastRelayFee = null;
620
641
  if (!cfg.api)
621
642
  return (await plan.execute()).bundles;
643
+ const paid = await withRelayFee(fides, actions, plan.estimatedSavings().estimatedTransactions);
644
+ if (typeof paid === 'string')
645
+ throw new Error(paid);
646
+ if (paid) {
647
+ actions = paid;
648
+ plan = await new FidesBuilder({ client, fides, agent }).add(actions).build();
649
+ }
622
650
  const stored = await post('/v1/plans/build', { fides, actions });
623
651
  if (stored.planHash !== plan.planHash)
624
652
  throw new Error('The relayer built a different plan from the same actions, so nothing was sent.');
@@ -968,7 +996,7 @@ export function createFidesMcpServer(cfg) {
968
996
  const sim = lp.bundle ? await la.simulate(lp) : { success: false, reason: lp.plan.rejected[0]?.reason ?? 'NOTHING_TO_SEND' };
969
997
  const id = (lp.bundleHash ?? '0x').slice(0, 18);
970
998
  if (sim.success)
971
- longPlans.set(id, { agent: la, plan: lp, fides, swap: { quote: bq, outDecimals: outInfo.decimals, outSymbol: outInfo.symbol } });
999
+ longPlans.set(id, { agent: la, plan: lp, fides, actions: swapActions(bq, `swap ${amount} ${tIn.symbol} to ${outInfo.symbol} on Uniswap`), swap: { quote: bq, outDecimals: outInfo.decimals, outSymbol: outInfo.symbol } });
972
1000
  return ok({
973
1001
  plan_id: sim.success ? id : null, ready_to_execute: sim.success,
974
1002
  routes_compared: [...quotes].map(([name, q]) => ({ route: name, quoted: fmt(q.quotedOut), verdict: name === bestName ? 'best quote, chosen' : 'lower quote' })).concat(excluded.map((x) => ({ route: x.route, quoted: '-', verdict: 'out: ' + x.why }))),
@@ -1184,13 +1212,18 @@ export function createFidesMcpServer(cfg) {
1184
1212
  const sw = longEntry.swap, f2 = longEntry.fides;
1185
1213
  const outBalance = () => !sw ? Promise.resolve(0n) : sw.quote.tokenOut === NATIVE ? client.public.getBalance({ address: f2.account }) : client.public.readContract({ address: sw.quote.tokenOut, abi: erc20Abi, functionName: 'balanceOf', args: [f2.account] });
1186
1214
  const before = await outBalance();
1187
- const r = await longEntry.agent.execute(longEntry.plan);
1215
+ lastRelayFee = null;
1216
+ const paid = longEntry.actions ? await withRelayFee(f2, longEntry.actions, 1) : null;
1217
+ if (typeof paid === 'string')
1218
+ return fail(paid);
1219
+ const r = await longEntry.agent.execute(paid ? await longEntry.agent.plan(paid) : longEntry.plan);
1188
1220
  const st = await longEntry.agent.status().catch(() => null), t = await token(f2.spendLimits[0].asset);
1189
1221
  const received = sw && r.status === 'EXECUTED' ? (await outBalance()) - before : 0n;
1190
1222
  return ok({
1191
1223
  result: r.status === 'EXECUTED' ? 'executed' : 'not executed',
1192
1224
  transactions: r.txHash || r.error ? [{ status: r.status.toLowerCase(), transaction: txLink(r.txHash), ...(r.error ? { reason: failure(r.error) } : {}) }] : [],
1193
1225
  blocked_and_not_sent: longEntry.plan.plan.rejected.map((a) => `${a.label}: ${REASONS[a.reason ?? ''] ?? a.reason}`),
1226
+ ...(lastRelayFee ? { relay_fee: `${lastRelayFee}, because the free relayed bundle for today is used` } : {}),
1194
1227
  ...(sw && r.status === 'EXECUTED' ? { best_execution: { quoted: `${formatUnits(sw.quote.quotedOut, sw.outDecimals)} ${sw.outSymbol}`, received: `${formatUnits(received, sw.outDecimals)} ${sw.outSymbol}`, route: describeRoute(sw.quote) } } : {}),
1195
1228
  ...(st ? { this_period: { spent: `${formatUnits(st.thisPeriod[0].spent, t.decimals)} ${t.symbol}`, left: `${formatUnits(st.thisPeriod[0].remaining, t.decimals)} ${t.symbol}`, resets_at: new Date(Number(st.resetsAt) * 1000).toISOString() }, total_left: `${formatUnits(st.total[0].remaining, t.decimals)} of ${formatUnits(st.total[0].limit, t.decimals)} ${t.symbol}` } : {}),
1196
1229
  });
@@ -1216,6 +1249,7 @@ export function createFidesMcpServer(cfg) {
1216
1249
  result: executed === bundles.length ? 'executed' : executed ? 'partly executed' : 'not executed',
1217
1250
  transactions: bundles.map((b) => ({ status: b.status.toLowerCase(), transaction: txLink(b.txHash), ...(b.error ? { reason: failure(b.error) } : {}) })),
1218
1251
  blocked_and_not_sent: plan.rejected().map((a) => `${a.label}: ${REASONS[a.reason ?? ''] ?? a.reason}`),
1252
+ ...(lastRelayFee ? { relay_fee: `${lastRelayFee}, because the free relayed bundle for today is used` } : {}),
1219
1253
  ...(swap && executed === bundles.length ? { best_execution: {
1220
1254
  quoted: `${formatUnits(swap.quote.quotedOut, swap.outDecimals)} ${swap.outSymbol}`,
1221
1255
  received: `${formatUnits(received, swap.outDecimals)} ${swap.outSymbol}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fidesbuild/mcp-server",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "FIDES as tools for Claude and any MCP client: plan and execute actions inside a signed FIDES, gas paid by the hosted relayer.",
5
5
  "keywords": [
6
6
  "fides",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@modelcontextprotocol/sdk": "^1.32.1",
39
- "@fidesbuild/sdk": "0.2.0",
39
+ "@fidesbuild/sdk": "0.2.7",
40
40
  "viem": "^2.21.0",
41
41
  "zod": "^4.6.5",
42
42
  "@fidesbuild/x402": "0.2.0",