@fidesbuild/mcp-server 0.6.0 → 0.8.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 +203 -17
  2. package/package.json +3 -2
package/dist/server.js CHANGED
@@ -20,6 +20,7 @@ import { Fides, FidesBuilder, Policy, Receipts, accountOf, createFidesClient, fr
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
+ import { quoteUniswap, checkMandate, swapActions, verifySwap, describeRoute, mandateOf, uniswap, NATIVE, SwapError } from '@fidesbuild/swap';
23
24
  export function configFromEnv(env = process.env) {
24
25
  const home = env.FIDES_HOME ?? path.join(os.homedir(), '.fides');
25
26
  return {
@@ -175,7 +176,7 @@ export function createFidesMcpServer(cfg) {
175
176
  ...(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.` } : {}),
176
177
  };
177
178
  }
178
- const server = new McpServer({ name: 'fides', version: '0.6.0' }, {
179
+ const server = new McpServer({ name: 'fides', version: '0.8.0' }, {
179
180
  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. ' +
180
181
  '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, ' +
181
182
  '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". ' +
@@ -199,6 +200,7 @@ export function createFidesMcpServer(cfg) {
199
200
  'Send USDG from the FIDES Account, to one address or many, inside the budget. Several transfers go out as one transaction.',
200
201
  'Call contracts the FIDES allows, with the functions it allows.',
201
202
  'Buy from paid APIs that charge per request over x402, paying from the FIDES budget, each capped per service.',
203
+ '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.',
202
204
  '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.',
203
205
  '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.',
204
206
  'Show the budget spent and left, what is allowed, the expiry, and every transaction executed, with explorer links.',
@@ -238,23 +240,64 @@ export function createFidesMcpServer(cfg) {
238
240
  });
239
241
  server.registerTool('fides_setup', {
240
242
  title: 'Set up FIDES',
241
- description: 'Use when the user wants to set up FIDES, connect their wallet or sign a new FIDES. Returns this computer\'s agent address and a link to the FIDES app with it filled in, where the user sets the limits and signs. The signed FIDES is then picked up automatically. To let the user buy from paid APIs (fides_pay_request), pass the services with a spending cap each. Paid APIs known to work use the ids "fides-demo", "spraay" and "madeonsol" (fides_help has their URLs and prices); for any other x402 API, the id is any name the user chooses.',
243
+ description: 'Use when the user wants to set up FIDES, connect their wallet or sign a new FIDES. Returns this computer\'s agent address and a link to the FIDES app with it filled in, where the user sets the limits and signs. The signed FIDES is then picked up automatically. To let the user buy from paid APIs (fides_pay_request), pass the services with a spending cap each. Paid APIs known to work use the ids "fides-demo", "spraay" and "madeonsol" (fides_help has their URLs and prices); for any other paid API, call fides_check_api with its URL and use the service_id it returns. To let the agent call any contract on the chain, pass contracts with the functions it may call by name (fides_inspect_contract lists them and flags the risky ones).',
242
244
  inputSchema: {
243
- services: z.array(z.object({ id: z.string().describe('A name for the paid service, chosen by the user, e.g. "fides-demo" or "spraay"'), max: z.string().describe('Most the agent may spend on it, in USDG, e.g. "0.1"') })).optional(),
245
+ services: z.array(z.object({ id: z.string().describe('The service id: "fides-demo", "spraay", "madeonsol", or for any other paid API the service_id fides_check_api returns (its host)'), max: z.string().describe('Most the agent may spend on it, in USDG, e.g. "0.1"') })).optional(),
246
+ contracts: z.array(z.object({
247
+ address: z.string().describe('The contract address'),
248
+ functions: z.array(z.string()).min(1).describe('Function names (e.g. "supply", "withdraw") or 4-byte selectors. fides_inspect_contract lists them'),
249
+ })).optional().describe('Any contracts on the chain the agent may call, and exactly which functions; everything else on them stays blocked'),
244
250
  net: z.boolean().optional().describe('true to allow FIDES NET: lets the FIDES publish netting roots on FidesNetRoots, which fides_net needs'),
245
251
  delegate: z.boolean().optional().describe('true to let this agent hand slices of the FIDES to other agents (SUB-FIDES), which fides_delegate needs'),
252
+ swap: z.object({
253
+ buy: z.array(z.string()).min(1).describe('Tokens the agent may buy: "ETH", "WETH" or addresses'),
254
+ max_slippage_percent: z.number().positive().max(50).optional().describe('The worst price it may accept against the quote, in percent. Default 1'),
255
+ max_per_swap: z.string().optional().describe('The most one swap may spend, in the budget token. Default "1"'),
256
+ }).optional().describe('Let the agent swap on Uniswap inside a trading mandate the owner signs; fides_swap needs it'),
246
257
  },
247
- }, async ({ services, net, delegate }) => ok({
248
- agent_address: agent.address,
249
- link: setupLink + (services ?? []).map((x) => `&service=${encodeURIComponent(x.id)}:${encodeURIComponent(x.max)}`).join('') + (net && netRoots[cfg.chainId] ? `&target=${netRoots[cfg.chainId]}:${PUBLISH_SELECTOR}` : '') + (delegate ? '&delegate=1' : ''),
250
- services: services?.length ? services.map((x) => `${x.id}: up to ${x.max} USDG`) : undefined,
251
- steps: [
252
- 'Open the link. It opens the FIDES app with this agent already filled in.',
253
- 'Connect your wallet. Set the budget, the contracts allowed and the expiry. Start small.',
254
- 'Sign (free), click Create FIDES Account (a little gas), and send the budget to the FIDES Account address shown.',
255
- 'Come back and ask me for what you want. I pick up the signed FIDES by myself.',
256
- ],
257
- }));
258
+ }, async ({ services, net, delegate, swap, contracts }) => {
259
+ let targets = '';
260
+ const added = [];
261
+ for (const c of contracts ?? []) {
262
+ if (!isAddress(c.address))
263
+ return fail(`"${c.address}" is not a contract address.`);
264
+ const sels = c.functions.filter((x) => /^0x[0-9a-fA-F]{8}$/.test(x)).map((x) => x.toLowerCase());
265
+ const names = c.functions.filter((x) => !/^0x[0-9a-fA-F]{8}$/.test(x));
266
+ let label = c.address;
267
+ if (names.length) {
268
+ let info;
269
+ try {
270
+ info = await inspect(c.address);
271
+ }
272
+ catch (err) {
273
+ return fail(`Could not read ${c.address}: ${err?.message ?? err}`);
274
+ }
275
+ if (!info.verified)
276
+ return fail(`${c.address} is not verified, so its functions cannot be found by name. Give their 4-byte selectors instead.`);
277
+ label = info.name ?? c.address;
278
+ for (const n of names) {
279
+ const fn = info.functions.find((x) => x.name === n || x.signature.replace(/ \w+(?=[,)])/g, '') === n);
280
+ if (!fn)
281
+ return fail(`${label} has no function "${n}" that changes anything. It has: ${info.functions.map((x) => x.name).join(', ')}.`);
282
+ sels.push(fn.selector);
283
+ }
284
+ }
285
+ targets += `&target=${getAddress(c.address)}:${[...new Set(sels)].join(',')}`;
286
+ added.push(`${label}: ${c.functions.join(', ')}`);
287
+ }
288
+ return ok({
289
+ agent_address: agent.address,
290
+ 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')}` : ''),
291
+ services: services?.length ? services.map((x) => `${x.id}: up to ${x.max} USDG`) : undefined,
292
+ contracts: added.length ? added : undefined,
293
+ steps: [
294
+ 'Open the link. It opens the FIDES app with this agent already filled in.',
295
+ 'Connect your wallet. Set the budget, the contracts allowed and the expiry. Start small.',
296
+ 'Sign (free), click Create FIDES Account (a little gas), and send the budget to the FIDES Account address shown.',
297
+ 'Come back and ask me for what you want. I pick up the signed FIDES by myself.',
298
+ ],
299
+ });
300
+ });
258
301
  server.registerTool('fides_use_file', {
259
302
  title: 'Use a FIDES file',
260
303
  description: 'Only needed if the user has a fides.json file: loads a signed FIDES from a path on this computer, checks that it is for this computer\'s agent key, and keeps a copy. FIDES signed in the app are picked up without this.',
@@ -503,7 +546,13 @@ export function createFidesMcpServer(cfg) {
503
546
  return fail(notLoaded());
504
547
  if (!fides.services.length)
505
548
  return fail(`This FIDES allows no paid services. To buy from a paid API, the user signs a new FIDES that includes one: call fides_setup with services, for example [{ id: "fides-demo", max: "0.1" }].`);
506
- const id = service ?? (fides.services.length === 1 ? fides.services[0].serviceId : undefined);
549
+ const host = (() => { try {
550
+ return new URL(url).host.toLowerCase().replace(/^www\./, '');
551
+ }
552
+ catch {
553
+ return '';
554
+ } })();
555
+ const id = service ?? (fides.services.some((x) => x.serviceId === host) ? host : fides.services.length === 1 ? fides.services[0].serviceId : undefined);
507
556
  if (!id)
508
557
  return fail(`Say which service this is. The FIDES allows: ${fides.services.map((x) => x.serviceId).join(', ')}.`);
509
558
  const rule = fides.services.find((x) => x.serviceId === id);
@@ -600,6 +649,134 @@ export function createFidesMcpServer(cfg) {
600
649
  }
601
650
  });
602
651
  // ---- SUB-FIDES ----
652
+ // ---- Connect anything ----
653
+ const lookupBase = () => cfg.api ?? cfg.directory ?? 'https://api.fidesbuilder.xyz';
654
+ async function inspect(address) {
655
+ const res = await fetch(`${lookupBase()}/v1/contracts/${address}`);
656
+ const json = fromJson(await res.text());
657
+ if (!res.ok || json?.error)
658
+ throw new Error(json?.error?.message ?? `HTTP ${res.status}`);
659
+ return json;
660
+ }
661
+ server.registerTool('fides_inspect_contract', {
662
+ title: 'What a contract lets an agent do',
663
+ description: 'Reads any contract on the chain from its verified source (proxies followed) and lists the functions that change something, in plain words, with risks flagged: "danger" (lasting approvals, handing control to another address, upgrades, debt), "warn" (the agent picks where funds go, or it sends ETH) and "admin" (operator-only; will fail for an agent). Use it before fides_setup with contracts, and recommend only the functions the user needs. Sends nothing.',
664
+ inputSchema: { address: z.string().describe('The contract address') },
665
+ }, async ({ address }) => {
666
+ try {
667
+ if (!isAddress(address))
668
+ return fail('That is not a contract address.');
669
+ const info = await inspect(address);
670
+ if (!info.verified)
671
+ return ok({ address: info.address, verified: false, note: 'Not verified on Sourcify, so its functions cannot be read. The user can still allow it by 4-byte selector.' });
672
+ return ok({
673
+ name: info.name, address: info.address, verified: true, proxy: info.proxy ?? undefined,
674
+ functions: info.functions.map((fn) => ({ name: fn.name, does: fn.about || undefined, signature: fn.signature, selector: fn.selector, risks: fn.risks.length ? fn.risks.map((r) => `${r.level}: ${r.why}`) : undefined })),
675
+ next: 'To allow some of these, call fides_setup with contracts: [{ address, functions: [names] }] and give the user the link.',
676
+ });
677
+ }
678
+ catch (err) {
679
+ return fail('Could not read the contract: ' + (err?.message ?? err));
680
+ }
681
+ });
682
+ server.registerTool('fides_check_api', {
683
+ title: 'Can FIDES pay this API?',
684
+ description: 'Calls a paid API\'s URL once, without paying, and reads its x402 price: whether a FIDES on Robinhood Chain can pay it, the price, who is paid, and the service id and a suggested cap to use with fides_setup. Use it when the user wants the agent to use a paid API that is not one of the known ones. Pays nothing.',
685
+ inputSchema: { url: z.string().describe('The API URL, https') },
686
+ }, async ({ url }) => {
687
+ try {
688
+ const res = await fetch(`${lookupBase()}/v1/services/probe`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: toJson({ url, budgetToken: (await currentFides().catch(() => null))?.spendLimits[0].asset ?? '0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168' }) });
689
+ const p = fromJson(await res.text());
690
+ if (!res.ok || p?.error)
691
+ return fail(p?.error?.message ?? `HTTP ${res.status}`);
692
+ if (!p.payable)
693
+ return ok({ url: p.url, payable: false, why: p.reason, charges_on: p.networks?.length ? p.networks : undefined });
694
+ const dec = p.offer.decimals ?? 6, sym = p.offer.symbol ?? 'USDG';
695
+ return ok({
696
+ url: p.url, payable: true, price: `${formatUnits(BigInt(p.offer.amount), dec)} ${sym} a request`, paid_to: p.offer.payTo, what_it_is: p.description,
697
+ service_id: p.serviceId, suggested_cap: `${formatUnits(BigInt(p.suggestedCap), dec)} ${sym} (10 requests)`,
698
+ next: `To allow it, call fides_setup with services: [{ id: "${p.serviceId}", max: "${formatUnits(BigInt(p.suggestedCap), dec)}" }]. Then fides_pay_request with this URL.`,
699
+ });
700
+ }
701
+ catch (err) {
702
+ return fail('Could not check the API: ' + (err?.message ?? err));
703
+ }
704
+ });
705
+ // ---- Uniswap ----
706
+ const tokenArg = (t) => (/^eth$/i.test(t) ? 'ETH' : /^weth$/i.test(t) ? uniswap[cfg.chainId]?.weth : isAddress(t) ? getAddress(t) : null);
707
+ server.registerTool('fides_swap', {
708
+ title: 'Swap on Uniswap inside the FIDES',
709
+ description: 'Gets the best route from Uniswap on Robinhood Chain (v2, v3 and v4 pools, including v4 pools with hooks such as the aggregator hooks) for the FIDES Account, checks it against the trading mandate the owner signed (tokens it may buy, worst price, most per swap), and simulates it to measure what would actually arrive. Sends nothing. Returns a plan_id for fides_execute, which sends an exact approval and the swap as one transaction; gas is paid by the relayer. Show the route, the quoted amount and the worst accepted amount to the user before executing. Needs a FIDES that allows swaps: fides_setup with swap.',
710
+ inputSchema: {
711
+ token_out: z.string().describe('What to buy: "ETH", "WETH" or a token address'),
712
+ amount: z.string().describe('How much of the budget token to spend, e.g. "1"'),
713
+ max_slippage_percent: z.number().positive().optional().describe('Worst price against the quote, in percent. Defaults to the most the FIDES allows'),
714
+ },
715
+ }, async ({ token_out, amount, max_slippage_percent }) => {
716
+ try {
717
+ const fides = await currentFides();
718
+ if (!fides)
719
+ return fail(notLoaded());
720
+ if (fides.agent.toLowerCase() !== agent.address.toLowerCase())
721
+ return fail(`The loaded FIDES is for agent ${fides.agent}, not this computer's agent key ${agent.address}.`);
722
+ const m = mandateOf(fides);
723
+ if (!m)
724
+ return fail('This FIDES does not allow swaps. The user signs one that does: call fides_setup with swap, for example { buy: ["ETH"], max_slippage_percent: 1, max_per_swap: "1" }, and give them the link.');
725
+ const out = tokenArg(token_out);
726
+ if (!out)
727
+ return fail('"token_out" must be "ETH", "WETH" or a token address.');
728
+ const budgetToken = fides.spendLimits[0].asset, tIn = await token(budgetToken);
729
+ let value;
730
+ try {
731
+ value = parseUnits(amount, tIn.decimals);
732
+ }
733
+ catch {
734
+ return fail(`"amount" must be a number of ${tIn.symbol}, like "1".`);
735
+ }
736
+ const slippageBps = max_slippage_percent ? Math.round(max_slippage_percent * 100) : m.maxSlippageBps;
737
+ let q, blocked;
738
+ if (cfg.api) {
739
+ const r = await post('/v1/swap/quote', { fides, tokenOut: out, amount: value, slippageBps });
740
+ q = r.quote;
741
+ blocked = r.mandate?.allowed ? null : { code: r.mandate.code, message: r.mandate.message };
742
+ }
743
+ else {
744
+ if (!process.env.UNISWAP_API_KEY)
745
+ return fail('Swaps need the FIDES API, or UNISWAP_API_KEY when running without it.');
746
+ q = await quoteUniswap({ apiKey: process.env.UNISWAP_API_KEY, fides, tokenOut: out, amount: value, slippageBps });
747
+ blocked = checkMandate(fides, q);
748
+ }
749
+ const outInfo = q.tokenOut === NATIVE ? { symbol: 'ETH', decimals: 18 } : await token(q.tokenOut);
750
+ const fmt = (v) => `${formatUnits(v, outInfo.decimals)} ${outInfo.symbol}`;
751
+ if (blocked)
752
+ return fail(`Blocked by the trading mandate: ${blocked.message} Nothing was sent.`);
753
+ const actions = swapActions(q, `swap ${amount} ${tIn.symbol} to ${outInfo.symbol} on Uniswap`);
754
+ const plan = await new FidesBuilder({ client, fides, agent }).add(actions).build();
755
+ if (plan.rejected().length) {
756
+ const r = plan.rejected()[0];
757
+ return fail(`Blocked by the FIDES: ${REASONS[r.reason ?? ''] ?? r.reason}. Nothing was sent.`);
758
+ }
759
+ const v = await verifySwap(client, fides, plan, q);
760
+ const id = plan.planHash.slice(0, 18);
761
+ if (v.ok)
762
+ plans.set(id, { plan, actions, fides, swap: { quote: q, outDecimals: outInfo.decimals, outSymbol: outInfo.symbol } });
763
+ return ok({
764
+ plan_id: v.ok ? id : null,
765
+ ready_to_execute: v.ok,
766
+ route: describeRoute(q),
767
+ spend: `${amount} ${tIn.symbol}`,
768
+ quoted: fmt(q.quotedOut),
769
+ worst_accepted: `${fmt(q.minOut)} (${q.slippageBps / 100}% below the quote)`,
770
+ simulated: v.ok ? `would receive ${fmt(v.received)}` : `refused: ${v.reason}`,
771
+ transactions: `${plan.estimatedSavings().baselineTransactions} onchain write(s) go out as ${plan.estimatedSavings().estimatedTransactions} transaction(s): an exact approval and the swap, nothing left approved`,
772
+ mandate: `may buy ${m.buy.join(', ')}; worst price ${m.maxSlippageBps / 100}%; most per swap ${formatUnits(m.maxPerSwap, tIn.decimals)} ${tIn.symbol}`,
773
+ sent: 'nothing has been sent',
774
+ });
775
+ }
776
+ catch (err) {
777
+ return fail(err instanceof SwapError ? `Uniswap: ${err.message}` : 'Could not get a swap: ' + (err?.shortMessage ?? err?.message ?? err));
778
+ }
779
+ });
603
780
  server.registerTool('fides_delegate', {
604
781
  title: 'Hand another agent a slice of the FIDES (SUB-FIDES)',
605
782
  description: 'This agent hands another agent a SUB-FIDES: its own budget, contracts and expiry, inside this FIDES. The child can never exceed it, and everything it spends also counts against this FIDES. This agent signs it; nothing is sent onchain and it costs nothing. The child agent (Claude or an SDK agent running FIDES) finds it by itself, or the user can hand it the file. Needs a FIDES that allows delegation: fides_setup with delegate: true. Confirm the amount and the agent with the user first.',
@@ -729,7 +906,7 @@ export function createFidesMcpServer(cfg) {
729
906
  const given = fs.existsSync(subsDir) ? fs.readdirSync(subsDir).filter((n) => n.endsWith('.json')).map((n) => fromJson(fs.readFileSync(path.join(subsDir, n), 'utf8')).sub) : [];
730
907
  const now = BigInt(Math.floor(Date.now() / 1000));
731
908
  return ok({
732
- handed_out: given.map((s) => ({ sub_fides: s.id, child_agent: s.agent, budget: s.spendLimits.map((l) => l.amount.toString()), expires: new Date(Number(s.validUntil) * 1000).toISOString(), expired: now >= s.validUntil })),
909
+ handed_out: await Promise.all(given.map(async (s) => ({ sub_fides: s.id, child_agent: s.agent, budget: (await Promise.all(s.spendLimits.map(async (l) => { const t = await token(l.asset); return `${formatUnits(l.amount, t.decimals)} ${t.symbol}`; }))).join(', '), expires: new Date(Number(s.validUntil) * 1000).toISOString(), expired: now >= s.validUntil }))),
733
910
  received: (await receivedSubs()).map((x) => ({ sub_fides: x.sub.id, delegated_by: x.parent.agent, expires: new Date(Number(x.sub.validUntil) * 1000).toISOString() })),
734
911
  });
735
912
  }
@@ -762,9 +939,12 @@ export function createFidesMcpServer(cfg) {
762
939
  if (!entry)
763
940
  return fail('No plan with that id. Make one with fides_plan first (plans are kept until they are executed or the server restarts).');
764
941
  plans.delete(plan_id); // a plan's bundles can be sent once
765
- const { plan, actions, fides, net } = entry;
942
+ const { plan, actions, fides, net, swap } = entry;
766
943
  try {
944
+ const outBalance = () => !swap ? Promise.resolve(0n) : swap.quote.tokenOut === NATIVE ? client.public.getBalance({ address: fides.account }) : client.public.readContract({ address: swap.quote.tokenOut, abi: erc20Abi, functionName: 'balanceOf', args: [fides.account] });
945
+ const before = await outBalance();
767
946
  const bundles = await send(plan, actions, fides);
947
+ const received = swap ? (await outBalance()) - before : 0n;
768
948
  const [spend] = await Policy.remainingSpend(client, fides);
769
949
  const t = await token(fides.spendLimits[0].asset);
770
950
  const executed = bundles.filter((b) => b.status === 'EXECUTED').length;
@@ -772,6 +952,12 @@ export function createFidesMcpServer(cfg) {
772
952
  result: executed === bundles.length ? 'executed' : executed ? 'partly executed' : 'not executed',
773
953
  transactions: bundles.map((b) => ({ status: b.status.toLowerCase(), transaction: txLink(b.txHash), ...(b.error ? { reason: failure(b.error) } : {}) })),
774
954
  blocked_and_not_sent: plan.rejected().map((a) => `${a.label}: ${REASONS[a.reason ?? ''] ?? a.reason}`),
955
+ ...(swap && executed === bundles.length ? { best_execution: {
956
+ quoted: `${formatUnits(swap.quote.quotedOut, swap.outDecimals)} ${swap.outSymbol}`,
957
+ received: `${formatUnits(received, swap.outDecimals)} ${swap.outSymbol}`,
958
+ versus_quote: received >= swap.quote.quotedOut ? 'at or better than the quote' : `${(Number(((swap.quote.quotedOut - received) * 10000n) / swap.quote.quotedOut) / 100).toFixed(2)}% below the quote, inside the ${swap.quote.slippageBps / 100}% allowed`,
959
+ route: describeRoute(swap.quote),
960
+ } } : {}),
775
961
  ...(net && executed === bundles.length ? { netting_record: { contract: netRoots[cfg.chainId], operator: fides.account, epoch: net.epoch.toString(), obligations_root: net.obligationsRoot, settlement_root: net.settlementRoot, contract_link: explorer ? `${explorer}/address/${netRoots[cfg.chainId]}` : undefined } } : {}),
776
962
  budget_remaining: `${formatUnits(spend.remaining, t.decimals)} of ${formatUnits(spend.limit, t.decimals)} ${t.symbol}`,
777
963
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fidesbuild/mcp-server",
3
- "version": "0.6.0",
3
+ "version": "0.8.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",
@@ -41,6 +41,7 @@
41
41
  "zod": "^4.6.5",
42
42
  "@fidesbuild/x402": "0.2.0",
43
43
  "@fidesbuild/net": "0.2.1",
44
- "@fidesbuild/sub": "0.2.0"
44
+ "@fidesbuild/sub": "0.2.0",
45
+ "@fidesbuild/swap": "0.2.0"
45
46
  }
46
47
  }