@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.
- package/dist/server.js +41 -7
- 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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|