@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.
- package/dist/server.js +203 -17
- 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.
|
|
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
|
|
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('
|
|
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 }) =>
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
}
|