@fidesbuild/mcp-server 0.9.0 → 0.11.0

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 +173 -18
  2. package/package.json +4 -3
package/dist/server.js CHANGED
@@ -16,12 +16,13 @@ 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';
23
23
  import { quoteUniswap, checkMandate, swapActions, verifySwap, describeRoute, mandateOf, uniswap, NATIVE, SwapError } from '@fidesbuild/swap';
24
24
  import { route } from '@fidesbuild/router';
25
+ import { LongAgent, LongFides, isLong } from '@fidesbuild/long';
25
26
  export function configFromEnv(env = process.env) {
26
27
  const home = env.FIDES_HOME ?? path.join(os.homedir(), '.fides');
27
28
  return {
@@ -142,6 +143,23 @@ export function createFidesMcpServer(cfg) {
142
143
  const subClient = subAddrs ? createSubClient({ chainId: cfg.chainId, rpc: cfg.rpc, base: cfg.addresses, addresses: subAddrs }) : null;
143
144
  const subPlans = new Map();
144
145
  const subsDir = path.join(cfg.home, 'subs');
146
+ // LONG FIDES: the terms (period, budget per period, calls per period) come with the FIDES from the API,
147
+ // and are kept here so a FIDES loaded from the file can still run.
148
+ const longFile = path.join(cfg.home, 'long.json');
149
+ const longTerms = new Map(Object.entries(fs.existsSync(longFile) ? fromJson(fs.readFileSync(longFile, 'utf8')) : {}));
150
+ const keepTerms = (id, terms) => { longTerms.set(id, terms); fs.mkdirSync(cfg.home, { recursive: true }); fs.writeFileSync(longFile, toJson(Object.fromEntries(longTerms), 2) + '\n'); };
151
+ const longPlans = new Map();
152
+ /** The agent for a LONG FIDES, or a message saying why it cannot run. */
153
+ const longAgentFor = (fides) => {
154
+ const terms = longTerms.get(fides.id);
155
+ if (!subClient)
156
+ return `LONG FIDES is not deployed on chain ${cfg.chainId}.`;
157
+ if (!terms)
158
+ return 'This is a LONG FIDES, but its terms (the budget per period) are not here. The app shares them when the owner signs; ask the user to open it in the FIDES app once, or sign it again.';
159
+ return new LongAgent({ client: subClient, fides, terms, agent, ...(cfg.api ? { api: cfg.api } : { relayer: agent }) });
160
+ };
161
+ const every = (t) => ({ 3600: 'hour', 86400: 'day', 604800: 'week', 2592000: '30 days' }[String(t.period)] ?? `${Number(t.period) / 3600} hours`);
162
+ const noLong = (what) => `This is a LONG FIDES. It swaps, pays paid APIs, sends and calls contracts inside its budget per period, but it does not ${what}. For that, the user signs an ordinary FIDES (fides_setup without keeps_going).`;
145
163
  const token = async (asset) => {
146
164
  const key = asset.toLowerCase();
147
165
  if (!tokens.has(key)) {
@@ -175,8 +193,11 @@ export function createFidesMcpServer(cfg) {
175
193
  const res = await fetch(`${cfg.directory}/v1/agents/${agent.address}/fides`);
176
194
  if (res.ok)
177
195
  for (const x of fromJson(await res.text()).fidesList ?? [])
178
- if (mine(x.fides) && x.status?.valid)
196
+ if (mine(x.fides) && x.status?.valid) {
179
197
  candidates.push(x.fides);
198
+ if (x.long)
199
+ keepTerms(x.fides.id, x.long);
200
+ }
180
201
  }
181
202
  catch { /* the API is optional: the copy kept here still works */ }
182
203
  }
@@ -203,9 +224,16 @@ export function createFidesMcpServer(cfg) {
203
224
  const amount = (v) => `${formatUnits(v, t.decimals)} ${t.symbol}`;
204
225
  const outside = await stray(fides.spendLimits[0].asset);
205
226
  const status = check.revoked ? 'revoked' : check.expired ? 'expired' : check.notYetValid ? 'not valid yet' : !check.validSignature || !check.idMatches ? 'signature does not verify' : 'active';
227
+ const terms = isLong(fides) ? longTerms.get(fides.id) : undefined;
228
+ const period = terms && subClient ? await LongFides.status(subClient, fides, terms).catch(() => null) : null;
206
229
  return {
207
230
  status,
208
231
  fides: fides.id,
232
+ ...(isLong(fides) ? { keeps_going: !terms ? 'a LONG FIDES, but its terms are not here yet' : {
233
+ budget_per_period: `${amount(terms.perPeriod[0].amount)} every ${every(terms)}`,
234
+ this_period: period ? { spent: amount(period.thisPeriod[0].spent), left: amount(period.thisPeriod[0].remaining), calls: `${period.actions.used} of ${terms.maxActionsPerPeriod}`, resets_at: new Date(Number(period.resetsAt) * 1000).toISOString() } : 'unknown',
235
+ note: 'The budget per period resets by itself. The total budget is the most it can ever spend.',
236
+ } } : {}),
209
237
  owner: fides.owner,
210
238
  agent: fides.agent,
211
239
  fides_account: fides.account,
@@ -223,12 +251,13 @@ export function createFidesMcpServer(cfg) {
223
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.` } : {}),
224
252
  };
225
253
  }
226
- const server = new McpServer({ name: 'fides', version: '0.8.0' }, {
254
+ const server = new McpServer({ name: 'fides', version: '0.11.0' }, {
227
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. ' +
228
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, ' +
229
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". ' +
230
258
  'When the user asks what you can do with FIDES, which paid APIs or services there are, or how to start, call fides_help and answer from it: give the paid APIs by name with their price and suggest a next step. ' +
231
259
  'A paid service\'s id is a name the user chooses when signing, like "spraay"; nobody issues it. Any API that charges USDG on Robinhood Chain over x402 (exact, EIP-3009) works, not only the ones fides_help lists. ' +
260
+ 'For a LONG FIDES (one that keeps going, with a budget per period), plans are checked against what is left this period and in total; if this period\'s budget is used up, say when it resets. ' +
232
261
  'When the user has many payments owed between their agent and services (or other parties), fides_net nets them to final positions and builds the FIDES Account\'s side of the settlement, plus the publication of the round\'s roots onchain, into one plan for fides_execute.',
233
262
  });
234
263
  server.registerTool('fides_help', {
@@ -250,6 +279,8 @@ export function createFidesMcpServer(cfg) {
250
279
  'Buy from paid APIs that charge per request over x402, paying from the FIDES budget, each capped per service.',
251
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.',
252
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.',
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.',
253
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.',
254
285
  'Show the budget spent and left, what is allowed, the expiry, and every transaction executed, with explorer links.',
255
286
  'Everything is planned and simulated first, and shown to the user before anything is sent. Anything outside the FIDES is blocked. The hosted relayer pays the gas.',
@@ -302,8 +333,18 @@ export function createFidesMcpServer(cfg) {
302
333
  max_slippage_percent: z.number().positive().max(50).optional().describe('The worst price it may accept against the quote, in percent. Default 1'),
303
334
  max_per_swap: z.string().optional().describe('The most one swap may spend, in the budget token. Default "1"'),
304
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.'),
337
+ keeps_going: z.object({
338
+ every: z.enum(['hour', 'day', 'week', 'month']).describe('How often the budget resets'),
339
+ per_period: z.string().describe('The most it may spend each period, in USDG, e.g. "5"'),
340
+ days: z.number().positive().max(365).optional().describe('How long it keeps going, in days. Default 30'),
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.'),
305
342
  },
306
- }, async ({ services, net, delegate, swap, contracts }) => {
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".');
346
+ if (keeps_going && (delegate || net))
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.');
307
348
  let targets = '';
308
349
  const added = [];
309
350
  for (const c of contracts ?? []) {
@@ -335,7 +376,9 @@ export function createFidesMcpServer(cfg) {
335
376
  }
336
377
  return ok({
337
378
  agent_address: agent.address,
338
- 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')}` : ''),
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,
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,
339
382
  services: services?.length ? services.map((x) => `${x.id}: up to ${x.max} USDG`) : undefined,
340
383
  contracts: added.length ? added : undefined,
341
384
  steps: [
@@ -453,6 +496,25 @@ export function createFidesMcpServer(cfg) {
453
496
  if ('error' in made)
454
497
  return fail(made.error);
455
498
  const built = made.built;
499
+ if (isLong(fides)) {
500
+ const la = longAgentFor(fides);
501
+ if (typeof la === 'string')
502
+ return fail(la);
503
+ const p = await la.plan(built);
504
+ const sim = p.bundle ? await la.simulate(p) : { success: false, reason: 'NOTHING_TO_SEND' };
505
+ const id = (p.bundleHash ?? '0x').slice(0, 18);
506
+ if (sim.success)
507
+ longPlans.set(id, { agent: la, plan: p, fides, actions: built });
508
+ return ok({
509
+ plan_id: sim.success ? id : null,
510
+ ready_to_execute: sim.success,
511
+ keeps_going: `period ${p.period}: checked against this period's budget and the total`,
512
+ actions: p.plan.actions.map((a) => ({ action: a.label, decision: a.decision === 'AUTHORIZED' ? 'inside the FIDES' : a.decision === 'REJECTED' ? `blocked: ${REASONS[a.reason ?? ''] ?? a.reason} (${a.reason}), counting this period's budget` : 'duplicate, sent once' })),
513
+ transactions: p.bundle ? `${p.bundle.calls.length} onchain write(s) go out as 1 transaction` : 'nothing to execute',
514
+ simulation: !p.bundle ? 'nothing to execute: no action fits this period\'s budget' : sim.success ? 'simulated against the chain: it would succeed' : `would fail: ${failure(sim.reason)}`,
515
+ sent: 'nothing has been sent',
516
+ });
517
+ }
456
518
  const plan = await new FidesBuilder({ client, fides, agent }).add(built).build();
457
519
  const savings = plan.estimatedSavings();
458
520
  const simulations = savings.estimatedTransactions ? await plan.simulate() : [];
@@ -490,6 +552,8 @@ export function createFidesMcpServer(cfg) {
490
552
  return fail(notLoaded());
491
553
  if (fides.agent.toLowerCase() !== agent.address.toLowerCase())
492
554
  return fail(`The loaded FIDES is for agent ${fides.agent}, not this computer's agent key ${agent.address}.`);
555
+ if (isLong(fides))
556
+ return fail(noLong('settle with FIDES NET'));
493
557
  const roots = netRoots[cfg.chainId];
494
558
  if (!roots)
495
559
  return fail(`FIDES NET is not deployed on chain ${cfg.chainId}.`);
@@ -557,9 +621,32 @@ export function createFidesMcpServer(cfg) {
557
621
  return json;
558
622
  }
559
623
  /** Sends a plan's bundles: through the hosted relayer, or paid by the agent key with FIDES_RELAY=self. */
624
+ // The hosted relayer pays gas for free up to a daily budget. 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).catch(() => null);
631
+ if (!fees?.free.exhausted)
632
+ return null;
633
+ if (!RelayFees.allows(fides, fees))
634
+ return 'The hosted relayer\'s free budget is used up 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
+ }
560
639
  async function send(plan, actions, fides) {
640
+ lastRelayFee = null;
561
641
  if (!cfg.api)
562
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
+ }
563
650
  const stored = await post('/v1/plans/build', { fides, actions });
564
651
  if (stored.planHash !== plan.planHash)
565
652
  throw new Error('The relayer built a different plan from the same actions, so nothing was sent.');
@@ -657,20 +744,36 @@ export function createFidesMcpServer(cfg) {
657
744
  let funded;
658
745
  if (float < price) {
659
746
  const actions = [x402.float(offer.asset, price - float, 'pay-float')];
660
- const plan = await new FidesBuilder({ client, fides, agent }).add(actions).build();
661
- if (plan.rejected().length) {
662
- const r = plan.rejected()[0];
663
- return fail(`Blocked by the FIDES: ${REASONS[r.reason ?? ''] ?? r.reason}. Nothing was paid.`);
747
+ if (isLong(fides)) {
748
+ const la = longAgentFor(fides);
749
+ if (typeof la === 'string')
750
+ return fail(la);
751
+ const lp = await la.plan(actions);
752
+ if (lp.plan.rejected.length)
753
+ return fail(`Blocked by the FIDES: ${REASONS[lp.plan.rejected[0].reason ?? ''] ?? lp.plan.rejected[0].reason}, counting this period's budget. Nothing was paid.`);
754
+ const r = await la.execute(lp);
755
+ if (r.status !== 'EXECUTED')
756
+ return fail('Could not move the price to the agent key: ' + failure(r.error) + ' Nothing was paid.');
757
+ funded = txLink(r.txHash);
758
+ float = price;
759
+ setFloat(fides, offer.asset, float);
760
+ }
761
+ const plan = isLong(fides) ? null : await new FidesBuilder({ client, fides, agent }).add(actions).build();
762
+ if (plan) {
763
+ if (plan.rejected().length) {
764
+ const r = plan.rejected()[0];
765
+ return fail(`Blocked by the FIDES: ${REASONS[r.reason ?? ''] ?? r.reason}. Nothing was paid.`);
766
+ }
767
+ const [sim] = await plan.simulate();
768
+ if (sim && !sim.success)
769
+ return fail('Blocked: ' + failure(sim.revertReason) + ' Nothing was paid.');
770
+ const bundles = await send(plan, actions, fides);
771
+ if (!bundles.length || bundles.some((b) => b.status !== 'EXECUTED'))
772
+ return fail('Could not move the price to the agent key: ' + failure(bundles.find((b) => b.error)?.error) + ' Nothing was paid.');
773
+ funded = txLink(bundles[0].txHash);
774
+ float = price;
775
+ setFloat(fides, offer.asset, float);
664
776
  }
665
- const [sim] = await plan.simulate();
666
- if (sim && !sim.success)
667
- return fail('Blocked: ' + failure(sim.revertReason) + ' Nothing was paid.');
668
- const bundles = await send(plan, actions, fides);
669
- if (!bundles.length || bundles.some((b) => b.status !== 'EXECUTED'))
670
- return fail('Could not move the price to the agent key: ' + failure(bundles.find((b) => b.error)?.error) + ' Nothing was paid.');
671
- funded = txLink(bundles[0].txHash);
672
- float = price;
673
- setFloat(fides, offer.asset, float);
674
777
  }
675
778
  // 3. pay and fetch
676
779
  const res = await x402.fetch(url);
@@ -882,6 +985,28 @@ export function createFidesMcpServer(cfg) {
882
985
  const mandate = excluded.find((x) => x.why.startsWith('trading mandate'));
883
986
  return fail(mandate ? `Blocked by the ${mandate.why} Nothing was sent.` : `Uniswap has no route for this swap: ${excluded.map((x) => `${x.route} (${x.why})`).join('; ')}. Nothing was sent.`);
884
987
  }
988
+ if (isLong(fides)) {
989
+ const la = longAgentFor(fides);
990
+ if (typeof la === 'string')
991
+ return fail(la);
992
+ const [bestName, bq] = [...quotes].sort((a, b) => (b[1].quotedOut > a[1].quotedOut ? 1 : -1))[0];
993
+ const outInfo = bq.tokenOut === NATIVE ? { symbol: 'ETH', decimals: 18 } : await token(bq.tokenOut);
994
+ const fmt = (v) => `${formatUnits(v, outInfo.decimals)} ${outInfo.symbol}`;
995
+ const lp = await la.plan(swapActions(bq, `swap ${amount} ${tIn.symbol} to ${outInfo.symbol} on Uniswap`));
996
+ const sim = lp.bundle ? await la.simulate(lp) : { success: false, reason: lp.plan.rejected[0]?.reason ?? 'NOTHING_TO_SEND' };
997
+ const id = (lp.bundleHash ?? '0x').slice(0, 18);
998
+ if (sim.success)
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 } });
1000
+ return ok({
1001
+ plan_id: sim.success ? id : null, ready_to_execute: sim.success,
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 }))),
1003
+ chosen: bestName, route: describeRoute(bq), spend: `${amount} ${tIn.symbol}`, quoted: fmt(bq.quotedOut),
1004
+ worst_accepted: `${fmt(bq.minOut)} (${bq.slippageBps / 100}% below the quote)`,
1005
+ keeps_going: `period ${lp.period}: checked against this period's budget and the total`,
1006
+ simulation: sim.success ? 'simulated against the chain: it would succeed' : `would fail: ${failure(sim.reason)}`,
1007
+ sent: 'nothing has been sent',
1008
+ });
1009
+ }
885
1010
  const first = [...quotes.values()][0];
886
1011
  const outInfo = first.tokenOut === NATIVE ? { symbol: 'ETH', decimals: 18 } : await token(first.tokenOut);
887
1012
  const fmt = (v) => `${formatUnits(v, outInfo.decimals)} ${outInfo.symbol}`;
@@ -941,6 +1066,8 @@ export function createFidesMcpServer(cfg) {
941
1066
  return fail('"agent" must be an address.');
942
1067
  if (child.toLowerCase() === agent.address.toLowerCase())
943
1068
  return fail('An agent cannot delegate to itself.');
1069
+ if (isLong(parent))
1070
+ return fail(noLong('hand out slices to other agents'));
944
1071
  const v2 = await accountOf(subClient, parent.owner);
945
1072
  if (v2.toLowerCase() !== parent.account.toLowerCase())
946
1073
  return fail('This FIDES cannot delegate: its FIDES Account is not one that supports SUB-FIDES. The user signs a new FIDES that allows delegation: call fides_setup with delegate: true and give them the link.');
@@ -1078,6 +1205,33 @@ export function createFidesMcpServer(cfg) {
1078
1205
  return fail('Execution stopped: ' + failure(err?.shortMessage ?? err?.message ?? err));
1079
1206
  }
1080
1207
  }
1208
+ const longEntry = longPlans.get(plan_id);
1209
+ if (longEntry) {
1210
+ longPlans.delete(plan_id);
1211
+ try {
1212
+ const sw = longEntry.swap, f2 = longEntry.fides;
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] });
1214
+ const before = await outBalance();
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);
1220
+ const st = await longEntry.agent.status().catch(() => null), t = await token(f2.spendLimits[0].asset);
1221
+ const received = sw && r.status === 'EXECUTED' ? (await outBalance()) - before : 0n;
1222
+ return ok({
1223
+ result: r.status === 'EXECUTED' ? 'executed' : 'not executed',
1224
+ transactions: r.txHash || r.error ? [{ status: r.status.toLowerCase(), transaction: txLink(r.txHash), ...(r.error ? { reason: failure(r.error) } : {}) }] : [],
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 relayer is full for today` } : {}),
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) } } : {}),
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}` } : {}),
1229
+ });
1230
+ }
1231
+ catch (err) {
1232
+ return fail('Execution stopped: ' + failure(err?.shortMessage ?? err?.message ?? err));
1233
+ }
1234
+ }
1081
1235
  const entry = plans.get(plan_id);
1082
1236
  if (!entry)
1083
1237
  return fail('No plan with that id. Make one with fides_plan first (plans are kept until they are executed or the server restarts).');
@@ -1095,6 +1249,7 @@ export function createFidesMcpServer(cfg) {
1095
1249
  result: executed === bundles.length ? 'executed' : executed ? 'partly executed' : 'not executed',
1096
1250
  transactions: bundles.map((b) => ({ status: b.status.toLowerCase(), transaction: txLink(b.txHash), ...(b.error ? { reason: failure(b.error) } : {}) })),
1097
1251
  blocked_and_not_sent: plan.rejected().map((a) => `${a.label}: ${REASONS[a.reason ?? ''] ?? a.reason}`),
1252
+ ...(lastRelayFee ? { relay_fee: `${lastRelayFee}, because the free relayer is full for today` } : {}),
1098
1253
  ...(swap && executed === bundles.length ? { best_execution: {
1099
1254
  quoted: `${formatUnits(swap.quote.quotedOut, swap.outDecimals)} ${swap.outSymbol}`,
1100
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.9.0",
3
+ "version": "0.11.0",
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,13 +36,14 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@modelcontextprotocol/sdk": "^1.32.1",
39
- "@fidesbuild/sdk": "0.2.0",
39
+ "@fidesbuild/sdk": "0.2.6",
40
40
  "viem": "^2.21.0",
41
41
  "zod": "^4.6.5",
42
42
  "@fidesbuild/x402": "0.2.0",
43
43
  "@fidesbuild/net": "0.2.1",
44
44
  "@fidesbuild/sub": "0.2.0",
45
45
  "@fidesbuild/swap": "0.2.1",
46
- "@fidesbuild/router": "0.2.0"
46
+ "@fidesbuild/router": "0.2.0",
47
+ "@fidesbuild/long": "0.2.0"
47
48
  }
48
49
  }