@fidesbuild/mcp-server 0.8.2 → 0.10.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 +185 -37
- package/package.json +4 -2
package/dist/server.js
CHANGED
|
@@ -21,6 +21,8 @@ import { createX402, decodePaymentRequired, exactEip3009 } from '@fidesbuild/x40
|
|
|
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
|
+
import { route } from '@fidesbuild/router';
|
|
25
|
+
import { LongAgent, LongFides, isLong } from '@fidesbuild/long';
|
|
24
26
|
export function configFromEnv(env = process.env) {
|
|
25
27
|
const home = env.FIDES_HOME ?? path.join(os.homedir(), '.fides');
|
|
26
28
|
return {
|
|
@@ -141,6 +143,23 @@ export function createFidesMcpServer(cfg) {
|
|
|
141
143
|
const subClient = subAddrs ? createSubClient({ chainId: cfg.chainId, rpc: cfg.rpc, base: cfg.addresses, addresses: subAddrs }) : null;
|
|
142
144
|
const subPlans = new Map();
|
|
143
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).`;
|
|
144
163
|
const token = async (asset) => {
|
|
145
164
|
const key = asset.toLowerCase();
|
|
146
165
|
if (!tokens.has(key)) {
|
|
@@ -174,8 +193,11 @@ export function createFidesMcpServer(cfg) {
|
|
|
174
193
|
const res = await fetch(`${cfg.directory}/v1/agents/${agent.address}/fides`);
|
|
175
194
|
if (res.ok)
|
|
176
195
|
for (const x of fromJson(await res.text()).fidesList ?? [])
|
|
177
|
-
if (mine(x.fides) && x.status?.valid)
|
|
196
|
+
if (mine(x.fides) && x.status?.valid) {
|
|
178
197
|
candidates.push(x.fides);
|
|
198
|
+
if (x.long)
|
|
199
|
+
keepTerms(x.fides.id, x.long);
|
|
200
|
+
}
|
|
179
201
|
}
|
|
180
202
|
catch { /* the API is optional: the copy kept here still works */ }
|
|
181
203
|
}
|
|
@@ -202,9 +224,16 @@ export function createFidesMcpServer(cfg) {
|
|
|
202
224
|
const amount = (v) => `${formatUnits(v, t.decimals)} ${t.symbol}`;
|
|
203
225
|
const outside = await stray(fides.spendLimits[0].asset);
|
|
204
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;
|
|
205
229
|
return {
|
|
206
230
|
status,
|
|
207
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
|
+
} } : {}),
|
|
208
237
|
owner: fides.owner,
|
|
209
238
|
agent: fides.agent,
|
|
210
239
|
fides_account: fides.account,
|
|
@@ -222,12 +251,13 @@ export function createFidesMcpServer(cfg) {
|
|
|
222
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.` } : {}),
|
|
223
252
|
};
|
|
224
253
|
}
|
|
225
|
-
const server = new McpServer({ name: 'fides', version: '0.
|
|
254
|
+
const server = new McpServer({ name: 'fides', version: '0.10.0' }, {
|
|
226
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. ' +
|
|
227
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, ' +
|
|
228
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". ' +
|
|
229
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. ' +
|
|
230
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. ' +
|
|
231
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.',
|
|
232
262
|
});
|
|
233
263
|
server.registerTool('fides_help', {
|
|
@@ -249,6 +279,7 @@ export function createFidesMcpServer(cfg) {
|
|
|
249
279
|
'Buy from paid APIs that charge per request over x402, paying from the FIDES budget, each capped per service.',
|
|
250
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.',
|
|
251
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 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.',
|
|
252
283
|
'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.',
|
|
253
284
|
'Show the budget spent and left, what is allowed, the expiry, and every transaction executed, with explorer links.',
|
|
254
285
|
'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.',
|
|
@@ -301,8 +332,15 @@ export function createFidesMcpServer(cfg) {
|
|
|
301
332
|
max_slippage_percent: z.number().positive().max(50).optional().describe('The worst price it may accept against the quote, in percent. Default 1'),
|
|
302
333
|
max_per_swap: z.string().optional().describe('The most one swap may spend, in the budget token. Default "1"'),
|
|
303
334
|
}).optional().describe('Let the agent swap on Uniswap inside a trading mandate the owner signs; fides_swap needs it'),
|
|
335
|
+
keeps_going: z.object({
|
|
336
|
+
every: z.enum(['hour', 'day', 'week', 'month']).describe('How often the budget resets'),
|
|
337
|
+
per_period: z.string().describe('The most it may spend each period, in USDG, e.g. "5"'),
|
|
338
|
+
days: z.number().positive().max(365).optional().describe('How long it keeps going, in days. Default 30'),
|
|
339
|
+
}).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.'),
|
|
304
340
|
},
|
|
305
|
-
}, async ({ services, net, delegate, swap, contracts }) => {
|
|
341
|
+
}, async ({ services, net, delegate, swap, contracts, keeps_going }) => {
|
|
342
|
+
if (keeps_going && (delegate || net))
|
|
343
|
+
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.');
|
|
306
344
|
let targets = '';
|
|
307
345
|
const added = [];
|
|
308
346
|
for (const c of contracts ?? []) {
|
|
@@ -334,7 +372,8 @@ export function createFidesMcpServer(cfg) {
|
|
|
334
372
|
}
|
|
335
373
|
return ok({
|
|
336
374
|
agent_address: agent.address,
|
|
337
|
-
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')}` : ''),
|
|
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}` : ''),
|
|
376
|
+
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,
|
|
338
377
|
services: services?.length ? services.map((x) => `${x.id}: up to ${x.max} USDG`) : undefined,
|
|
339
378
|
contracts: added.length ? added : undefined,
|
|
340
379
|
steps: [
|
|
@@ -452,6 +491,25 @@ export function createFidesMcpServer(cfg) {
|
|
|
452
491
|
if ('error' in made)
|
|
453
492
|
return fail(made.error);
|
|
454
493
|
const built = made.built;
|
|
494
|
+
if (isLong(fides)) {
|
|
495
|
+
const la = longAgentFor(fides);
|
|
496
|
+
if (typeof la === 'string')
|
|
497
|
+
return fail(la);
|
|
498
|
+
const p = await la.plan(built);
|
|
499
|
+
const sim = p.bundle ? await la.simulate(p) : { success: false, reason: 'NOTHING_TO_SEND' };
|
|
500
|
+
const id = (p.bundleHash ?? '0x').slice(0, 18);
|
|
501
|
+
if (sim.success)
|
|
502
|
+
longPlans.set(id, { agent: la, plan: p, fides });
|
|
503
|
+
return ok({
|
|
504
|
+
plan_id: sim.success ? id : null,
|
|
505
|
+
ready_to_execute: sim.success,
|
|
506
|
+
keeps_going: `period ${p.period}: checked against this period's budget and the total`,
|
|
507
|
+
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' })),
|
|
508
|
+
transactions: p.bundle ? `${p.bundle.calls.length} onchain write(s) go out as 1 transaction` : 'nothing to execute',
|
|
509
|
+
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)}`,
|
|
510
|
+
sent: 'nothing has been sent',
|
|
511
|
+
});
|
|
512
|
+
}
|
|
455
513
|
const plan = await new FidesBuilder({ client, fides, agent }).add(built).build();
|
|
456
514
|
const savings = plan.estimatedSavings();
|
|
457
515
|
const simulations = savings.estimatedTransactions ? await plan.simulate() : [];
|
|
@@ -489,6 +547,8 @@ export function createFidesMcpServer(cfg) {
|
|
|
489
547
|
return fail(notLoaded());
|
|
490
548
|
if (fides.agent.toLowerCase() !== agent.address.toLowerCase())
|
|
491
549
|
return fail(`The loaded FIDES is for agent ${fides.agent}, not this computer's agent key ${agent.address}.`);
|
|
550
|
+
if (isLong(fides))
|
|
551
|
+
return fail(noLong('settle with FIDES NET'));
|
|
492
552
|
const roots = netRoots[cfg.chainId];
|
|
493
553
|
if (!roots)
|
|
494
554
|
return fail(`FIDES NET is not deployed on chain ${cfg.chainId}.`);
|
|
@@ -656,20 +716,36 @@ export function createFidesMcpServer(cfg) {
|
|
|
656
716
|
let funded;
|
|
657
717
|
if (float < price) {
|
|
658
718
|
const actions = [x402.float(offer.asset, price - float, 'pay-float')];
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
719
|
+
if (isLong(fides)) {
|
|
720
|
+
const la = longAgentFor(fides);
|
|
721
|
+
if (typeof la === 'string')
|
|
722
|
+
return fail(la);
|
|
723
|
+
const lp = await la.plan(actions);
|
|
724
|
+
if (lp.plan.rejected.length)
|
|
725
|
+
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.`);
|
|
726
|
+
const r = await la.execute(lp);
|
|
727
|
+
if (r.status !== 'EXECUTED')
|
|
728
|
+
return fail('Could not move the price to the agent key: ' + failure(r.error) + ' Nothing was paid.');
|
|
729
|
+
funded = txLink(r.txHash);
|
|
730
|
+
float = price;
|
|
731
|
+
setFloat(fides, offer.asset, float);
|
|
732
|
+
}
|
|
733
|
+
const plan = isLong(fides) ? null : await new FidesBuilder({ client, fides, agent }).add(actions).build();
|
|
734
|
+
if (plan) {
|
|
735
|
+
if (plan.rejected().length) {
|
|
736
|
+
const r = plan.rejected()[0];
|
|
737
|
+
return fail(`Blocked by the FIDES: ${REASONS[r.reason ?? ''] ?? r.reason}. Nothing was paid.`);
|
|
738
|
+
}
|
|
739
|
+
const [sim] = await plan.simulate();
|
|
740
|
+
if (sim && !sim.success)
|
|
741
|
+
return fail('Blocked: ' + failure(sim.revertReason) + ' Nothing was paid.');
|
|
742
|
+
const bundles = await send(plan, actions, fides);
|
|
743
|
+
if (!bundles.length || bundles.some((b) => b.status !== 'EXECUTED'))
|
|
744
|
+
return fail('Could not move the price to the agent key: ' + failure(bundles.find((b) => b.error)?.error) + ' Nothing was paid.');
|
|
745
|
+
funded = txLink(bundles[0].txHash);
|
|
746
|
+
float = price;
|
|
747
|
+
setFloat(fides, offer.asset, float);
|
|
663
748
|
}
|
|
664
|
-
const [sim] = await plan.simulate();
|
|
665
|
-
if (sim && !sim.success)
|
|
666
|
-
return fail('Blocked: ' + failure(sim.revertReason) + ' Nothing was paid.');
|
|
667
|
-
const bundles = await send(plan, actions, fides);
|
|
668
|
-
if (!bundles.length || bundles.some((b) => b.status !== 'EXECUTED'))
|
|
669
|
-
return fail('Could not move the price to the agent key: ' + failure(bundles.find((b) => b.error)?.error) + ' Nothing was paid.');
|
|
670
|
-
funded = txLink(bundles[0].txHash);
|
|
671
|
-
float = price;
|
|
672
|
-
setFloat(fides, offer.asset, float);
|
|
673
749
|
}
|
|
674
750
|
// 3. pay and fetch
|
|
675
751
|
const res = await x402.fetch(url);
|
|
@@ -822,7 +898,7 @@ export function createFidesMcpServer(cfg) {
|
|
|
822
898
|
const tokenArg = (t) => (/^eth$/i.test(t) ? 'ETH' : /^weth$/i.test(t) ? uniswap[cfg.chainId]?.weth : isAddress(t) ? getAddress(t) : null);
|
|
823
899
|
server.registerTool('fides_swap', {
|
|
824
900
|
title: 'Swap on Uniswap inside the FIDES',
|
|
825
|
-
description: '
|
|
901
|
+
description: 'Swaps on Uniswap on Robinhood Chain with FIDES ROUTER: gets a route through each kind of pool (v2, v3 and v4, including v4 pools with hooks), checks each against the FIDES and the trading mandate the owner signed (tokens it may buy, worst price, most per swap), simulates them, ranks them by output, and offers only the best valid one. Show the user routes_compared (the ranking, how far behind each route is, and why any are out) with the chosen route, before executing. 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.',
|
|
826
902
|
inputSchema: {
|
|
827
903
|
token_out: z.string().describe('What to buy: "ETH", "WETH" or a token address'),
|
|
828
904
|
amount: z.string().describe('How much of the budget token to spend, e.g. "1"'),
|
|
@@ -850,28 +926,73 @@ export function createFidesMcpServer(cfg) {
|
|
|
850
926
|
return fail(`"amount" must be a number of ${tIn.symbol}, like "1".`);
|
|
851
927
|
}
|
|
852
928
|
const slippageBps = max_slippage_percent ? Math.round(max_slippage_percent * 100) : m.maxSlippageBps;
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
929
|
+
if (!cfg.api && !process.env.UNISWAP_API_KEY)
|
|
930
|
+
return fail('Swaps need the FIDES API, or UNISWAP_API_KEY when running without it.');
|
|
931
|
+
// FIDES ROUTER: a route through each kind of Uniswap pool, each checked against the trading mandate and the
|
|
932
|
+
// FIDES, simulated and ranked by output. Only the best valid one is offered to execute.
|
|
933
|
+
const POOLS = [['uniswap v2', ['V2']], ['uniswap v3', ['V3']], ['uniswap v4', ['V4']]];
|
|
934
|
+
const quotes = new Map(), excluded = [];
|
|
935
|
+
await Promise.all(POOLS.map(async ([name, protocols]) => {
|
|
936
|
+
try {
|
|
937
|
+
let q, blocked;
|
|
938
|
+
if (cfg.api) {
|
|
939
|
+
const r = await post('/v1/swap/quote', { fides, tokenOut: out, amount: value, slippageBps, protocols });
|
|
940
|
+
q = r.quote;
|
|
941
|
+
blocked = r.mandate?.allowed ? null : { code: r.mandate.code, message: r.mandate.message };
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
q = await quoteUniswap({ apiKey: process.env.UNISWAP_API_KEY, fides, tokenOut: out, amount: value, slippageBps, protocols });
|
|
945
|
+
blocked = checkMandate(fides, q);
|
|
946
|
+
}
|
|
947
|
+
if (blocked)
|
|
948
|
+
excluded.push({ route: name, why: `trading mandate: ${blocked.message}` });
|
|
949
|
+
else
|
|
950
|
+
quotes.set(name, q);
|
|
951
|
+
}
|
|
952
|
+
catch (err) {
|
|
953
|
+
excluded.push({ route: name, why: `no route: ${err?.message ?? err}` });
|
|
954
|
+
}
|
|
955
|
+
}));
|
|
956
|
+
if (!quotes.size) {
|
|
957
|
+
const mandate = excluded.find((x) => x.why.startsWith('trading mandate'));
|
|
958
|
+
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.`);
|
|
858
959
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
960
|
+
if (isLong(fides)) {
|
|
961
|
+
const la = longAgentFor(fides);
|
|
962
|
+
if (typeof la === 'string')
|
|
963
|
+
return fail(la);
|
|
964
|
+
const [bestName, bq] = [...quotes].sort((a, b) => (b[1].quotedOut > a[1].quotedOut ? 1 : -1))[0];
|
|
965
|
+
const outInfo = bq.tokenOut === NATIVE ? { symbol: 'ETH', decimals: 18 } : await token(bq.tokenOut);
|
|
966
|
+
const fmt = (v) => `${formatUnits(v, outInfo.decimals)} ${outInfo.symbol}`;
|
|
967
|
+
const lp = await la.plan(swapActions(bq, `swap ${amount} ${tIn.symbol} to ${outInfo.symbol} on Uniswap`));
|
|
968
|
+
const sim = lp.bundle ? await la.simulate(lp) : { success: false, reason: lp.plan.rejected[0]?.reason ?? 'NOTHING_TO_SEND' };
|
|
969
|
+
const id = (lp.bundleHash ?? '0x').slice(0, 18);
|
|
970
|
+
if (sim.success)
|
|
971
|
+
longPlans.set(id, { agent: la, plan: lp, fides, swap: { quote: bq, outDecimals: outInfo.decimals, outSymbol: outInfo.symbol } });
|
|
972
|
+
return ok({
|
|
973
|
+
plan_id: sim.success ? id : null, ready_to_execute: sim.success,
|
|
974
|
+
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 }))),
|
|
975
|
+
chosen: bestName, route: describeRoute(bq), spend: `${amount} ${tIn.symbol}`, quoted: fmt(bq.quotedOut),
|
|
976
|
+
worst_accepted: `${fmt(bq.minOut)} (${bq.slippageBps / 100}% below the quote)`,
|
|
977
|
+
keeps_going: `period ${lp.period}: checked against this period's budget and the total`,
|
|
978
|
+
simulation: sim.success ? 'simulated against the chain: it would succeed' : `would fail: ${failure(sim.reason)}`,
|
|
979
|
+
sent: 'nothing has been sent',
|
|
980
|
+
});
|
|
864
981
|
}
|
|
865
|
-
const
|
|
982
|
+
const first = [...quotes.values()][0];
|
|
983
|
+
const outInfo = first.tokenOut === NATIVE ? { symbol: 'ETH', decimals: 18 } : await token(first.tokenOut);
|
|
866
984
|
const fmt = (v) => `${formatUnits(v, outInfo.decimals)} ${outInfo.symbol}`;
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
985
|
+
const label = `swap ${amount} ${tIn.symbol} to ${outInfo.symbol} on Uniswap`;
|
|
986
|
+
const routed = await route({ client, fides, agent }, [...quotes].map(([name, q]) => ({ name, actions: swapActions(q, label), expectedOut: q.quotedOut, minOut: q.minOut })));
|
|
987
|
+
const ranking = [
|
|
988
|
+
...routed.ranked.map((x) => x.valid
|
|
989
|
+
? { route: x.name, verdict: x === routed.best ? 'best, chosen' : 'valid', quoted: fmt(x.expectedOut ?? 0n), behind_best: `${(x.slippageBps ?? 0) / 100}%`, gas: String(x.gas), pools: describeRoute(quotes.get(x.name)) }
|
|
990
|
+
: { route: x.name, verdict: 'out', why: REASONS[x.reasons[0]?.split(': ')[1] ?? ''] ?? x.reasons[0] }),
|
|
991
|
+
...excluded.map((x) => ({ route: x.route, verdict: 'out', why: x.why })),
|
|
992
|
+
];
|
|
993
|
+
if (!routed.best)
|
|
994
|
+
return fail(`No route is inside the FIDES, so nothing can run: ${ranking.map((x) => `${x.route} (${x.why})`).join('; ')}. Nothing was sent.`);
|
|
995
|
+
const q = quotes.get(routed.best.name), plan = routed.plan(routed.best.name), actions = swapActions(q, label);
|
|
875
996
|
const v = await verifySwap(client, fides, plan, q);
|
|
876
997
|
const id = plan.planHash.slice(0, 18);
|
|
877
998
|
if (v.ok)
|
|
@@ -879,6 +1000,9 @@ export function createFidesMcpServer(cfg) {
|
|
|
879
1000
|
return ok({
|
|
880
1001
|
plan_id: v.ok ? id : null,
|
|
881
1002
|
ready_to_execute: v.ok,
|
|
1003
|
+
router: `FIDES ROUTER compared ${ranking.length} routes, one per kind of Uniswap pool: each checked against the FIDES and the trading mandate, simulated, and ranked by output. Only the chosen route runs.`,
|
|
1004
|
+
routes_compared: ranking,
|
|
1005
|
+
chosen: routed.best.name,
|
|
882
1006
|
route: describeRoute(q),
|
|
883
1007
|
spend: `${amount} ${tIn.symbol}`,
|
|
884
1008
|
quoted: fmt(q.quotedOut),
|
|
@@ -914,6 +1038,8 @@ export function createFidesMcpServer(cfg) {
|
|
|
914
1038
|
return fail('"agent" must be an address.');
|
|
915
1039
|
if (child.toLowerCase() === agent.address.toLowerCase())
|
|
916
1040
|
return fail('An agent cannot delegate to itself.');
|
|
1041
|
+
if (isLong(parent))
|
|
1042
|
+
return fail(noLong('hand out slices to other agents'));
|
|
917
1043
|
const v2 = await accountOf(subClient, parent.owner);
|
|
918
1044
|
if (v2.toLowerCase() !== parent.account.toLowerCase())
|
|
919
1045
|
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.');
|
|
@@ -1051,6 +1177,28 @@ export function createFidesMcpServer(cfg) {
|
|
|
1051
1177
|
return fail('Execution stopped: ' + failure(err?.shortMessage ?? err?.message ?? err));
|
|
1052
1178
|
}
|
|
1053
1179
|
}
|
|
1180
|
+
const longEntry = longPlans.get(plan_id);
|
|
1181
|
+
if (longEntry) {
|
|
1182
|
+
longPlans.delete(plan_id);
|
|
1183
|
+
try {
|
|
1184
|
+
const sw = longEntry.swap, f2 = longEntry.fides;
|
|
1185
|
+
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
|
+
const before = await outBalance();
|
|
1187
|
+
const r = await longEntry.agent.execute(longEntry.plan);
|
|
1188
|
+
const st = await longEntry.agent.status().catch(() => null), t = await token(f2.spendLimits[0].asset);
|
|
1189
|
+
const received = sw && r.status === 'EXECUTED' ? (await outBalance()) - before : 0n;
|
|
1190
|
+
return ok({
|
|
1191
|
+
result: r.status === 'EXECUTED' ? 'executed' : 'not executed',
|
|
1192
|
+
transactions: r.txHash || r.error ? [{ status: r.status.toLowerCase(), transaction: txLink(r.txHash), ...(r.error ? { reason: failure(r.error) } : {}) }] : [],
|
|
1193
|
+
blocked_and_not_sent: longEntry.plan.plan.rejected.map((a) => `${a.label}: ${REASONS[a.reason ?? ''] ?? a.reason}`),
|
|
1194
|
+
...(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
|
+
...(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
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
catch (err) {
|
|
1199
|
+
return fail('Execution stopped: ' + failure(err?.shortMessage ?? err?.message ?? err));
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1054
1202
|
const entry = plans.get(plan_id);
|
|
1055
1203
|
if (!entry)
|
|
1056
1204
|
return fail('No plan with that id. Make one with fides_plan first (plans are kept until they are executed or the server restarts).');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fidesbuild/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
"@fidesbuild/x402": "0.2.0",
|
|
43
43
|
"@fidesbuild/net": "0.2.1",
|
|
44
44
|
"@fidesbuild/sub": "0.2.0",
|
|
45
|
-
"@fidesbuild/swap": "0.2.
|
|
45
|
+
"@fidesbuild/swap": "0.2.1",
|
|
46
|
+
"@fidesbuild/router": "0.2.0",
|
|
47
|
+
"@fidesbuild/long": "0.2.0"
|
|
46
48
|
}
|
|
47
49
|
}
|