@fidesbuild/mcp-server 0.4.1 → 0.5.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.
Files changed (2) hide show
  1. package/dist/server.js +89 -6
  2. package/package.json +3 -2
package/dist/server.js CHANGED
@@ -18,6 +18,7 @@ import { encodeFunctionData, erc20Abi, formatUnits, parseUnits, isAddress, getAd
18
18
  import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
19
19
  import { Fides, FidesBuilder, Policy, Receipts, createFidesClient, fromJson, toJson } from '@fidesbuild/sdk';
20
20
  import { createX402, decodePaymentRequired, exactEip3009 } from '@fidesbuild/x402';
21
+ import { netObligations, settle, netRoots, PUBLISH_SELECTOR, NetError } from '@fidesbuild/net';
21
22
  export function configFromEnv(env = process.env) {
22
23
  const home = env.FIDES_HOME ?? path.join(os.homedir(), '.fides');
23
24
  return {
@@ -167,12 +168,13 @@ export function createFidesMcpServer(cfg) {
167
168
  ...(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.` } : {}),
168
169
  };
169
170
  }
170
- const server = new McpServer({ name: 'fides', version: '0.4.1' }, {
171
+ const server = new McpServer({ name: 'fides', version: '0.5.1' }, {
171
172
  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. ' +
172
173
  '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, ' +
173
174
  '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". ' +
174
175
  '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. ' +
175
- '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.',
176
+ '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. ' +
177
+ '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.',
176
178
  });
177
179
  server.registerTool('fides_help', {
178
180
  title: 'What FIDES can do',
@@ -190,6 +192,7 @@ export function createFidesMcpServer(cfg) {
190
192
  'Send USDG from the FIDES Account, to one address or many, inside the budget. Several transfers go out as one transaction.',
191
193
  'Call contracts the FIDES allows, with the functions it allows.',
192
194
  'Buy from paid APIs that charge per request over x402, paying from the FIDES budget, each capped per service.',
195
+ '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.',
193
196
  'Show the budget spent and left, what is allowed, the expiry, and every transaction executed, with explorer links.',
194
197
  '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.',
195
198
  ],
@@ -228,10 +231,13 @@ export function createFidesMcpServer(cfg) {
228
231
  server.registerTool('fides_setup', {
229
232
  title: 'Set up FIDES',
230
233
  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.',
231
- inputSchema: { 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() },
232
- }, async ({ services }) => ok({
234
+ inputSchema: {
235
+ 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(),
236
+ net: z.boolean().optional().describe('true to allow FIDES NET: lets the FIDES publish netting roots on FidesNetRoots, which fides_net needs'),
237
+ },
238
+ }, async ({ services, net }) => ok({
233
239
  agent_address: agent.address,
234
- link: setupLink + (services ?? []).map((x) => `&service=${encodeURIComponent(x.id)}:${encodeURIComponent(x.max)}`).join(''),
240
+ link: setupLink + (services ?? []).map((x) => `&service=${encodeURIComponent(x.id)}:${encodeURIComponent(x.max)}`).join('') + (net && netRoots[cfg.chainId] ? `&target=${netRoots[cfg.chainId]}:${PUBLISH_SELECTOR}` : ''),
235
241
  services: services?.length ? services.map((x) => `${x.id}: up to ${x.max} USDG`) : undefined,
236
242
  steps: [
237
243
  'Open the link. It opens the FIDES app with this agent already filled in.',
@@ -352,6 +358,82 @@ export function createFidesMcpServer(cfg) {
352
358
  return fail('Could not build the plan: ' + (err?.shortMessage ?? err?.message ?? err));
353
359
  }
354
360
  });
361
+ const obligationSchema = z.object({
362
+ from: z.string().describe('Who owes: an address, or "me" for this FIDES Account'),
363
+ to: z.string().describe('Who is owed: an address, or "me" for this FIDES Account'),
364
+ amount: z.string().describe('How much, in whole token units, e.g. "1.5"'),
365
+ ref: z.string().optional().describe('What it was for, e.g. a job or invoice id. Goes into the obligations root'),
366
+ });
367
+ server.registerTool('fides_net', {
368
+ title: 'Net and settle what is owed (FIDES NET)',
369
+ description: 'Takes the payments owed between this FIDES Account ("me") and other parties, nets them to one final position per party, and builds the FIDES Account\'s side of the settlement into one plan: its net transfers plus the publication of the round\'s two roots (every obligation, every settling transfer) on FidesNetRoots, so payments and the public record go out in the same transaction. Checks every action against the FIDES and simulates. Sends nothing: returns a plan_id for fides_execute. Transfers owed by other parties are listed, not sent. Show the result to the user before executing.',
370
+ inputSchema: { obligations: z.array(obligationSchema).min(1).max(200), token: z.string().optional().describe('The token address. Defaults to the FIDES\'s budget token') },
371
+ }, async ({ obligations, token: tokenArg }) => {
372
+ try {
373
+ const fides = await currentFides();
374
+ if (!fides)
375
+ return fail(notLoaded());
376
+ if (fides.agent.toLowerCase() !== agent.address.toLowerCase())
377
+ return fail(`The loaded FIDES is for agent ${fides.agent}, not this computer's agent key ${agent.address}.`);
378
+ const roots = netRoots[cfg.chainId];
379
+ if (!roots)
380
+ return fail(`FIDES NET is not deployed on chain ${cfg.chainId}.`);
381
+ const allowsRoots = fides.targets.some((t) => t.target.toLowerCase() === roots.toLowerCase() && t.selectors.some((x) => x.toLowerCase() === PUBLISH_SELECTOR));
382
+ if (!allowsRoots)
383
+ return fail(`This FIDES does not allow FIDES NET to publish its roots on ${roots}. The user signs a new FIDES that does: call fides_setup with net: true (and the same services, if any) and give them the link.`);
384
+ const asset = tokenArg ? (isAddress(tokenArg) ? getAddress(tokenArg) : null) : fides.spendLimits[0].asset;
385
+ if (!asset)
386
+ return fail('"token" must be an address.');
387
+ const t = await token(asset);
388
+ const party = (x, n, side) => {
389
+ if (x.trim().toLowerCase() === 'me')
390
+ return fides.account;
391
+ if (!isAddress(x))
392
+ throw new NetError(`Obligation ${n}: "${side}" must be an address or "me".`);
393
+ return getAddress(x);
394
+ };
395
+ const list = obligations.map((o, i) => {
396
+ let amount;
397
+ try {
398
+ amount = parseUnits(o.amount, t.decimals);
399
+ }
400
+ catch {
401
+ throw new NetError(`Obligation ${i + 1}: "amount" must be a number of ${t.symbol}, like "1.5".`);
402
+ }
403
+ return { from: party(o.from, i + 1, 'from'), to: party(o.to, i + 1, 'to'), asset, amount, fidesId: fides.id, ref: o.ref ?? `${i + 1}` };
404
+ });
405
+ const s = netObligations(list);
406
+ const p = settle(s, { account: fides.account, chainId: cfg.chainId, decimals: t.decimals, symbol: t.symbol });
407
+ const plan = await new FidesBuilder({ client, fides, agent }).add(p.actions).build();
408
+ const savings = plan.estimatedSavings();
409
+ const simulations = savings.estimatedTransactions ? await plan.simulate() : [];
410
+ const failed = simulations.filter((x) => !x.success);
411
+ const ready = Boolean(savings.estimatedTransactions && !failed.length && !plan.rejected().length);
412
+ const id = plan.planHash.slice(0, 18);
413
+ if (ready)
414
+ plans.set(id, { plan, actions: p.actions, fides, net: { epoch: p.epoch, obligationsRoot: s.obligationsRoot, settlementRoot: s.settlementRoot } });
415
+ const amt = (v) => `${formatUnits(v < 0n ? -v : v, t.decimals)} ${t.symbol}`;
416
+ const name = (a) => (a.toLowerCase() === fides.account.toLowerCase() ? 'me' : getAddress(a));
417
+ const book = s.positions[asset.toLowerCase()] ?? {};
418
+ return ok({
419
+ plan_id: ready ? id : null,
420
+ ready_to_execute: ready,
421
+ gross: `${s.gross.obligations} obligations, ${amt(s.gross.volume[asset.toLowerCase()] ?? 0n)} moving`,
422
+ net: `${s.net.transfers} transfer(s), ${amt(s.net.volume[asset.toLowerCase()] ?? 0n)} moving`,
423
+ positions: Object.entries(book).map(([who, v]) => `${name(who)}: ${v < 0n ? 'pays' : 'receives'} ${amt(v)}`),
424
+ my_transfers: p.own.map((x) => `pay ${amt(x.amount)} to ${getAddress(x.to)}`),
425
+ settled_by_others: p.others.map((x) => `${name(x.from)} pays ${amt(x.amount)} to ${name(x.to)} (not sent by this FIDES)`),
426
+ actions: 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})` : 'duplicate, sent once' })),
427
+ transactions: `${savings.baselineTransactions} onchain write(s) go out as ${savings.estimatedTransactions} transaction(s)`,
428
+ record: { contract: roots, epoch: p.epoch.toString(), obligations_root: s.obligationsRoot, settlement_root: s.settlementRoot },
429
+ simulation: !simulations.length ? 'nothing to execute' : failed.length ? `would fail: ${failure(failed[0].revertReason)}` : 'simulated against the chain: it would succeed',
430
+ sent: 'nothing has been sent',
431
+ });
432
+ }
433
+ catch (err) {
434
+ return fail(err instanceof NetError ? err.message : 'Could not net these: ' + (err?.shortMessage ?? err?.message ?? err));
435
+ }
436
+ });
355
437
  async function post(route, body) {
356
438
  const res = await fetch(cfg.api + route, { method: 'POST', headers: { 'content-type': 'application/json' }, body: toJson(body) });
357
439
  const json = fromJson(await res.text());
@@ -510,7 +592,7 @@ export function createFidesMcpServer(cfg) {
510
592
  if (!entry)
511
593
  return fail('No plan with that id. Make one with fides_plan first (plans are kept until they are executed or the server restarts).');
512
594
  plans.delete(plan_id); // a plan's bundles can be sent once
513
- const { plan, actions, fides } = entry;
595
+ const { plan, actions, fides, net } = entry;
514
596
  try {
515
597
  const bundles = await send(plan, actions, fides);
516
598
  const [spend] = await Policy.remainingSpend(client, fides);
@@ -520,6 +602,7 @@ export function createFidesMcpServer(cfg) {
520
602
  result: executed === bundles.length ? 'executed' : executed ? 'partly executed' : 'not executed',
521
603
  transactions: bundles.map((b) => ({ status: b.status.toLowerCase(), transaction: txLink(b.txHash), ...(b.error ? { reason: failure(b.error) } : {}) })),
522
604
  blocked_and_not_sent: plan.rejected().map((a) => `${a.label}: ${REASONS[a.reason ?? ''] ?? a.reason}`),
605
+ ...(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 } } : {}),
523
606
  budget_remaining: `${formatUnits(spend.remaining, t.decimals)} of ${formatUnits(spend.limit, t.decimals)} ${t.symbol}`,
524
607
  });
525
608
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fidesbuild/mcp-server",
3
- "version": "0.4.1",
3
+ "version": "0.5.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",
@@ -39,6 +39,7 @@
39
39
  "@fidesbuild/sdk": "0.2.0",
40
40
  "viem": "^2.21.0",
41
41
  "zod": "^4.6.5",
42
- "@fidesbuild/x402": "0.2.0"
42
+ "@fidesbuild/x402": "0.2.0",
43
+ "@fidesbuild/net": "0.2.1"
43
44
  }
44
45
  }