@fidesbuild/mcp-server 0.4.0 → 0.5.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 (3) hide show
  1. package/README.md +12 -1
  2. package/dist/server.js +133 -9
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -42,7 +42,8 @@ Claude Code and other MCP apps need [Node](https://nodejs.org) 20.10 or later. C
42
42
  | `fides_execute` | Executes a plan by its id. Only the actions inside the FIDES are sent |
43
43
  | `fides_pay_request` | Buys from a paid API over x402: checks the price against the service's cap in the FIDES, pays from the FIDES, returns the response |
44
44
  | `fides_receipts` | Every transaction executed under the FIDES, with explorer links |
45
- | `fides_use_file` | Loads an `fides.json` file, for a FIDES that was not signed in the app |
45
+ | `fides_help` | What you can do with FIDES, the paid APIs known to work with their prices, and what to try |
46
+ | `fides_use_file` | Loads a `fides.json` file, for a FIDES that was not signed in the app |
46
47
 
47
48
  The FIDES used is the newest valid one signed for this agent: from the FIDES API, where the app shares a FIDES when it is signed, or from the copy kept in `~/.fides`. Each is verified onchain before it is used.
48
49
 
@@ -50,6 +51,16 @@ The FIDES used is the newest valid one signed for this agent: from the FIDES API
50
51
 
51
52
  Claude can buy from APIs that charge per request over x402, from your FIDES. Each paid service is named in the FIDES with a cap; anything not named, or past its cap, is refused before paying. To try it, ask Claude: "Set up FIDES with the fides-demo paid service, up to 0.1 USDG", sign, then "Buy the chain stats from the FIDES demo" (0.01 USDG for Robinhood Chain's latest block, gas price and USDG supply).
52
53
 
54
+ Paid APIs known to work, each bought through FIDES with real payments on mainnet:
55
+
56
+ | Service id | URL | Price |
57
+ | --- | --- | --- |
58
+ | `fides-demo` | `https://api.fidesbuilder.xyz/v1/demo/chain-stats` | 0.01 USDG |
59
+ | `spraay` | `https://gateway.spraay.app/api/v1/models` | 0.001 USDG |
60
+ | `madeonsol` | `https://madeonsol.com/api/x402/rhc/kol/feed?limit=5` | 0.04 USDG |
61
+
62
+ Any API that charges USDG on Robinhood Chain over x402 (`exact`, EIP-3009) works the same way. The service id is any name you choose when you sign. Ask Claude "What can you do with FIDES?" to get this list in the chat.
63
+
53
64
  ## Other MCP apps
54
65
 
55
66
  Add a server with the command `npx` and the arguments `-y @fidesbuild/mcp-server@latest` (on Windows, the command `cmd` and the arguments `/c npx -y @fidesbuild/mcp-server@latest`). `npx -y @fidesbuild/mcp-server setup` makes the agent key and prints the setup link from a terminal.
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 {
@@ -71,6 +72,12 @@ function failure(reason) {
71
72
  return 'The hosted relayer cannot pay for gas right now. Try later, or set FIDES_RELAY=self and give the agent address a little ETH.';
72
73
  return r;
73
74
  }
75
+ // Paid APIs that have been bought through FIDES with real payments on mainnet (the docs list the same ones).
76
+ const KNOWN_SERVICES = [
77
+ { id: 'fides-demo', name: 'FIDES demo', url: 'https://api.fidesbuilder.xyz/v1/demo/chain-stats', price: '0.01 USDG', returns: 'Robinhood Chain\'s latest block, gas price and USDG supply', run_by: 'FIDES' },
78
+ { id: 'spraay', name: 'Spraay', url: 'https://gateway.spraay.app/api/v1/models', price: '0.001 USDG', returns: 'The AI models the Spraay gateway offers, with providers and prices', run_by: 'Spraay, not FIDES' },
79
+ { id: 'madeonsol', name: 'MadeOnSol', url: 'https://madeonsol.com/api/x402/rhc/kol/feed?limit=5', price: '0.04 USDG', returns: 'Recent trades on Robinhood Chain by tracked wallets', run_by: 'MadeOnSol, not FIDES' },
80
+ ];
74
81
  const ok = (value) => ({ content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }] });
75
82
  const fail = (message) => ({ content: [{ type: 'text', text: message }], isError: true });
76
83
  export function createFidesMcpServer(cfg) {
@@ -161,10 +168,47 @@ export function createFidesMcpServer(cfg) {
161
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.` } : {}),
162
169
  };
163
170
  }
164
- const server = new McpServer({ name: 'fides', version: '0.4.0' }, {
165
- instructions: 'FIDES lets you act with the owner\'s funds only inside limits the owner signed (a FIDES): a budget, allowed contracts and functions, and an expiry. ' +
171
+ const server = new McpServer({ name: 'fides', version: '0.5.0' }, {
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. ' +
166
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, ' +
167
- '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
+ '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". ' +
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. ' +
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.',
178
+ });
179
+ server.registerTool('fides_help', {
180
+ title: 'What FIDES can do',
181
+ description: 'Use when the user asks what you can do with FIDES, what paid APIs or services are available, what to try, or how to get started. Returns what FIDES can do, paid APIs known to work with their URL and price, example requests, and the next step for this user.',
182
+ inputSchema: {},
183
+ }, async () => {
184
+ let fides = null;
185
+ try {
186
+ fides = await currentFides();
187
+ }
188
+ catch { /* answer without it */ }
189
+ const allowed = fides?.services.map((s) => s.serviceId) ?? [];
190
+ return ok({
191
+ can_do: [
192
+ 'Send USDG from the FIDES Account, to one address or many, inside the budget. Several transfers go out as one transaction.',
193
+ 'Call contracts the FIDES allows, with the functions it allows.',
194
+ 'Buy from paid APIs that charge per request over x402, paying from the FIDES budget, each capped per service.',
195
+ 'Show the budget spent and left, what is allowed, the expiry, and every transaction executed, with explorer links.',
196
+ '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.',
197
+ ],
198
+ paid_apis: KNOWN_SERVICES.map((s) => ({ ...s, in_this_fides: allowed.includes(s.id) })),
199
+ other_paid_apis: 'Any API that charges USDG on Robinhood Chain over x402 (exact scheme, EIP-3009) works the same way. The user adds it as a service with a cap when signing (the id is any name they choose), then gives you its URL.',
200
+ try_saying: [
201
+ 'Set up FIDES with these paid services: fides-demo up to 0.1 USDG, spraay up to 0.01 USDG, madeonsol up to 0.1 USDG',
202
+ 'Pay 0.5 USDG to 0x... and 0.5 USDG to 0x...',
203
+ 'Buy the chain stats from the FIDES demo',
204
+ 'What\'s left in my FIDES?',
205
+ 'Show what you\'ve executed',
206
+ ],
207
+ next_step: !fides ? 'No FIDES is set up yet. Offer to call fides_setup, with any paid services the user wants and a cap for each.'
208
+ : !allowed.length ? 'This FIDES has no paid services. To use a paid API, call fides_setup with the services and caps, and the user signs the new FIDES.'
209
+ : `This FIDES allows these paid services: ${allowed.join(', ')}. Make sure the FIDES Account holds USDG (fides_status shows the balance).`,
210
+ docs: 'https://fidesbuilder.xyz/docs#/claude',
211
+ });
168
212
  });
169
213
  server.registerTool('fides_status', {
170
214
  title: 'FIDES status',
@@ -185,11 +229,14 @@ export function createFidesMcpServer(cfg) {
185
229
  });
186
230
  server.registerTool('fides_setup', {
187
231
  title: 'Set up FIDES',
188
- 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; for FIDES\'s own paid demo use id "fides-demo".',
189
- inputSchema: { services: z.array(z.object({ id: z.string().describe('A name for the paid service, e.g. "fides-demo"'), max: z.string().describe('Most the agent may spend on it, in USDG, e.g. "0.1"') })).optional() },
190
- }, async ({ services }) => ok({
232
+ 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.',
233
+ inputSchema: {
234
+ 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(),
235
+ net: z.boolean().optional().describe('true to allow FIDES NET: lets the FIDES publish netting roots on FidesNetRoots, which fides_net needs'),
236
+ },
237
+ }, async ({ services, net }) => ok({
191
238
  agent_address: agent.address,
192
- link: setupLink + (services ?? []).map((x) => `&service=${encodeURIComponent(x.id)}:${encodeURIComponent(x.max)}`).join(''),
239
+ link: setupLink + (services ?? []).map((x) => `&service=${encodeURIComponent(x.id)}:${encodeURIComponent(x.max)}`).join('') + (net && netRoots[cfg.chainId] ? `&target=${netRoots[cfg.chainId]}:${PUBLISH_SELECTOR}` : ''),
193
240
  services: services?.length ? services.map((x) => `${x.id}: up to ${x.max} USDG`) : undefined,
194
241
  steps: [
195
242
  'Open the link. It opens the FIDES app with this agent already filled in.',
@@ -310,6 +357,82 @@ export function createFidesMcpServer(cfg) {
310
357
  return fail('Could not build the plan: ' + (err?.shortMessage ?? err?.message ?? err));
311
358
  }
312
359
  });
360
+ const obligationSchema = z.object({
361
+ from: z.string().describe('Who owes: an address, or "me" for this FIDES Account'),
362
+ to: z.string().describe('Who is owed: an address, or "me" for this FIDES Account'),
363
+ amount: z.string().describe('How much, in whole token units, e.g. "1.5"'),
364
+ ref: z.string().optional().describe('What it was for, e.g. a job or invoice id. Goes into the obligations root'),
365
+ });
366
+ server.registerTool('fides_net', {
367
+ title: 'Net and settle what is owed (FIDES NET)',
368
+ 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.',
369
+ inputSchema: { obligations: z.array(obligationSchema).min(1).max(200), token: z.string().optional().describe('The token address. Defaults to the FIDES\'s budget token') },
370
+ }, async ({ obligations, token: tokenArg }) => {
371
+ try {
372
+ const fides = await currentFides();
373
+ if (!fides)
374
+ return fail(notLoaded());
375
+ if (fides.agent.toLowerCase() !== agent.address.toLowerCase())
376
+ return fail(`The loaded FIDES is for agent ${fides.agent}, not this computer's agent key ${agent.address}.`);
377
+ const roots = netRoots[cfg.chainId];
378
+ if (!roots)
379
+ return fail(`FIDES NET is not deployed on chain ${cfg.chainId}.`);
380
+ const allowsRoots = fides.targets.some((t) => t.target.toLowerCase() === roots.toLowerCase() && t.selectors.some((x) => x.toLowerCase() === PUBLISH_SELECTOR));
381
+ if (!allowsRoots)
382
+ 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.`);
383
+ const asset = tokenArg ? (isAddress(tokenArg) ? getAddress(tokenArg) : null) : fides.spendLimits[0].asset;
384
+ if (!asset)
385
+ return fail('"token" must be an address.');
386
+ const t = await token(asset);
387
+ const party = (x, n, side) => {
388
+ if (x.trim().toLowerCase() === 'me')
389
+ return fides.account;
390
+ if (!isAddress(x))
391
+ throw new NetError(`Obligation ${n}: "${side}" must be an address or "me".`);
392
+ return getAddress(x);
393
+ };
394
+ const list = obligations.map((o, i) => {
395
+ let amount;
396
+ try {
397
+ amount = parseUnits(o.amount, t.decimals);
398
+ }
399
+ catch {
400
+ throw new NetError(`Obligation ${i + 1}: "amount" must be a number of ${t.symbol}, like "1.5".`);
401
+ }
402
+ 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}` };
403
+ });
404
+ const s = netObligations(list);
405
+ const p = settle(s, { account: fides.account, chainId: cfg.chainId });
406
+ const plan = await new FidesBuilder({ client, fides, agent }).add(p.actions).build();
407
+ const savings = plan.estimatedSavings();
408
+ const simulations = savings.estimatedTransactions ? await plan.simulate() : [];
409
+ const failed = simulations.filter((x) => !x.success);
410
+ const ready = Boolean(savings.estimatedTransactions && !failed.length && !plan.rejected().length);
411
+ const id = plan.planHash.slice(0, 18);
412
+ if (ready)
413
+ plans.set(id, { plan, actions: p.actions, fides, net: { epoch: p.epoch, obligationsRoot: s.obligationsRoot, settlementRoot: s.settlementRoot } });
414
+ const amt = (v) => `${formatUnits(v < 0n ? -v : v, t.decimals)} ${t.symbol}`;
415
+ const name = (a) => (a.toLowerCase() === fides.account.toLowerCase() ? 'me' : getAddress(a));
416
+ const book = s.positions[asset.toLowerCase()] ?? {};
417
+ return ok({
418
+ plan_id: ready ? id : null,
419
+ ready_to_execute: ready,
420
+ gross: `${s.gross.obligations} obligations, ${amt(s.gross.volume[asset.toLowerCase()] ?? 0n)} moving`,
421
+ net: `${s.net.transfers} transfer(s), ${amt(s.net.volume[asset.toLowerCase()] ?? 0n)} moving`,
422
+ positions: Object.entries(book).map(([who, v]) => `${name(who)}: ${v < 0n ? 'pays' : 'receives'} ${amt(v)}`),
423
+ my_transfers: p.own.map((x) => `pay ${amt(x.amount)} to ${getAddress(x.to)}`),
424
+ settled_by_others: p.others.map((x) => `${name(x.from)} pays ${amt(x.amount)} to ${name(x.to)} (not sent by this FIDES)`),
425
+ 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' })),
426
+ transactions: `${savings.baselineTransactions} onchain write(s) go out as ${savings.estimatedTransactions} transaction(s)`,
427
+ record: { contract: roots, epoch: p.epoch.toString(), obligations_root: s.obligationsRoot, settlement_root: s.settlementRoot },
428
+ simulation: !simulations.length ? 'nothing to execute' : failed.length ? `would fail: ${failure(failed[0].revertReason)}` : 'simulated against the chain: it would succeed',
429
+ sent: 'nothing has been sent',
430
+ });
431
+ }
432
+ catch (err) {
433
+ return fail(err instanceof NetError ? err.message : 'Could not net these: ' + (err?.shortMessage ?? err?.message ?? err));
434
+ }
435
+ });
313
436
  async function post(route, body) {
314
437
  const res = await fetch(cfg.api + route, { method: 'POST', headers: { 'content-type': 'application/json' }, body: toJson(body) });
315
438
  const json = fromJson(await res.text());
@@ -350,7 +473,7 @@ export function createFidesMcpServer(cfg) {
350
473
  };
351
474
  server.registerTool('fides_pay_request', {
352
475
  title: 'Buy from a paid API',
353
- description: 'Fetches a URL from a paid API that charges per request over x402, paying from the FIDES budget. Reads the price first and refuses if it is over the service\'s cap in the FIDES or over max_price. Moves exactly the price from the FIDES Account to the agent key, pays, and returns the response. The service must be in the FIDES (its services list). For FIDES\'s own demo, the URL is https://api.fidesbuilder.xyz/v1/demo/chain-stats and the service is "fides-demo". Tell the user the price and get their OK before paying unless they already asked you to buy it.',
476
+ description: `Fetches a URL from a paid API that charges per request over x402, paying from the FIDES budget. Reads the price first and refuses if it is over the service\'s cap in the FIDES or over max_price. Moves exactly the price from the FIDES Account to the agent key, pays, and returns the response. The service must be in the FIDES (its services list). Known URLs: ${KNOWN_SERVICES.map((s) => `"${s.id}" ${s.url}`).join(', ')}. Tell the user the price and get their OK before paying unless they already asked you to buy it.`,
354
477
  inputSchema: {
355
478
  url: z.string().describe('The paid API URL'),
356
479
  service: z.string().optional().describe('The service id in the FIDES. Can be left out if the FIDES has exactly one service'),
@@ -468,7 +591,7 @@ export function createFidesMcpServer(cfg) {
468
591
  if (!entry)
469
592
  return fail('No plan with that id. Make one with fides_plan first (plans are kept until they are executed or the server restarts).');
470
593
  plans.delete(plan_id); // a plan's bundles can be sent once
471
- const { plan, actions, fides } = entry;
594
+ const { plan, actions, fides, net } = entry;
472
595
  try {
473
596
  const bundles = await send(plan, actions, fides);
474
597
  const [spend] = await Policy.remainingSpend(client, fides);
@@ -478,6 +601,7 @@ export function createFidesMcpServer(cfg) {
478
601
  result: executed === bundles.length ? 'executed' : executed ? 'partly executed' : 'not executed',
479
602
  transactions: bundles.map((b) => ({ status: b.status.toLowerCase(), transaction: txLink(b.txHash), ...(b.error ? { reason: failure(b.error) } : {}) })),
480
603
  blocked_and_not_sent: plan.rejected().map((a) => `${a.label}: ${REASONS[a.reason ?? ''] ?? a.reason}`),
604
+ ...(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 } } : {}),
481
605
  budget_remaining: `${formatUnits(spend.remaining, t.decimals)} of ${formatUnits(spend.limit, t.decimals)} ${t.symbol}`,
482
606
  });
483
607
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fidesbuild/mcp-server",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",
@@ -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.0"
43
44
  }
44
45
  }