@fidesbuild/mcp-server 0.5.1 → 0.6.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.d.ts +3 -0
- package/dist/server.js +216 -46
- package/package.json +3 -2
package/dist/server.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
3
3
|
import { type Hex } from 'viem';
|
|
4
4
|
import { type PrivateKeyAccount } from 'viem/accounts';
|
|
5
5
|
import { type FidesAddresses } from '@fidesbuild/sdk';
|
|
6
|
+
import { type SubAddresses } from '@fidesbuild/sub';
|
|
6
7
|
export type Config = {
|
|
7
8
|
/** Folder for agent.json and the default fides.json. */
|
|
8
9
|
home: string;
|
|
@@ -16,6 +17,8 @@ export type Config = {
|
|
|
16
17
|
app: string;
|
|
17
18
|
fidesFile: string;
|
|
18
19
|
addresses?: FidesAddresses;
|
|
20
|
+
/** SUB-FIDES contracts, when not the published ones for the chain. */
|
|
21
|
+
sub?: SubAddresses;
|
|
19
22
|
agentKey?: Hex;
|
|
20
23
|
};
|
|
21
24
|
export declare function configFromEnv(env?: NodeJS.ProcessEnv): Config;
|
package/dist/server.js
CHANGED
|
@@ -16,9 +16,10 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { encodeFunctionData, erc20Abi, formatUnits, parseUnits, isAddress, getAddress } from 'viem';
|
|
18
18
|
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
19
|
-
import { Fides, FidesBuilder, Policy, Receipts, createFidesClient, fromJson, toJson } from '@fidesbuild/sdk';
|
|
19
|
+
import { Fides, FidesBuilder, Policy, Receipts, 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
|
+
import { createSubClient, SubFidesKit, SubAgent, SubFidesError, subDeployments } from '@fidesbuild/sub';
|
|
22
23
|
export function configFromEnv(env = process.env) {
|
|
23
24
|
const home = env.FIDES_HOME ?? path.join(os.homedir(), '.fides');
|
|
24
25
|
return {
|
|
@@ -30,6 +31,7 @@ export function configFromEnv(env = process.env) {
|
|
|
30
31
|
app: (env.FIDES_APP ?? 'https://fidesbuilder.xyz/app').replace(/\/$/, ''),
|
|
31
32
|
fidesFile: env.FIDES_FILE ?? path.join(home, 'fides.json'),
|
|
32
33
|
addresses: env.FIDES_ADDRESSES ? JSON.parse(fs.readFileSync(env.FIDES_ADDRESSES, 'utf8')) : undefined,
|
|
34
|
+
sub: env.FIDES_SUB_ADDRESSES ? JSON.parse(fs.readFileSync(env.FIDES_SUB_ADDRESSES, 'utf8')) : undefined,
|
|
33
35
|
agentKey: env.FIDES_AGENT_KEY,
|
|
34
36
|
};
|
|
35
37
|
}
|
|
@@ -87,6 +89,11 @@ export function createFidesMcpServer(cfg) {
|
|
|
87
89
|
const txLink = (h) => (h && explorer ? `${explorer}/tx/${h}` : h);
|
|
88
90
|
const plans = new Map();
|
|
89
91
|
const tokens = new Map();
|
|
92
|
+
// SUB-FIDES: a FIDES on a V2 account can hand slices of itself to other agents.
|
|
93
|
+
const subAddrs = cfg.sub ?? subDeployments[cfg.chainId];
|
|
94
|
+
const subClient = subAddrs ? createSubClient({ chainId: cfg.chainId, rpc: cfg.rpc, base: cfg.addresses, addresses: subAddrs }) : null;
|
|
95
|
+
const subPlans = new Map();
|
|
96
|
+
const subsDir = path.join(cfg.home, 'subs');
|
|
90
97
|
const token = async (asset) => {
|
|
91
98
|
const key = asset.toLowerCase();
|
|
92
99
|
if (!tokens.has(key)) {
|
|
@@ -168,7 +175,7 @@ export function createFidesMcpServer(cfg) {
|
|
|
168
175
|
...(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.` } : {}),
|
|
169
176
|
};
|
|
170
177
|
}
|
|
171
|
-
const server = new McpServer({ name: 'fides', version: '0.
|
|
178
|
+
const server = new McpServer({ name: 'fides', version: '0.6.0' }, {
|
|
172
179
|
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. ' +
|
|
173
180
|
'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, ' +
|
|
174
181
|
'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". ' +
|
|
@@ -192,6 +199,7 @@ export function createFidesMcpServer(cfg) {
|
|
|
192
199
|
'Send USDG from the FIDES Account, to one address or many, inside the budget. Several transfers go out as one transaction.',
|
|
193
200
|
'Call contracts the FIDES allows, with the functions it allows.',
|
|
194
201
|
'Buy from paid APIs that charge per request over x402, paying from the FIDES budget, each capped per service.',
|
|
202
|
+
'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.',
|
|
195
203
|
'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.',
|
|
196
204
|
'Show the budget spent and left, what is allowed, the expiry, and every transaction executed, with explorer links.',
|
|
197
205
|
'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.',
|
|
@@ -234,10 +242,11 @@ export function createFidesMcpServer(cfg) {
|
|
|
234
242
|
inputSchema: {
|
|
235
243
|
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
244
|
net: z.boolean().optional().describe('true to allow FIDES NET: lets the FIDES publish netting roots on FidesNetRoots, which fides_net needs'),
|
|
245
|
+
delegate: z.boolean().optional().describe('true to let this agent hand slices of the FIDES to other agents (SUB-FIDES), which fides_delegate needs'),
|
|
237
246
|
},
|
|
238
|
-
}, async ({ services, net }) => ok({
|
|
247
|
+
}, async ({ services, net, delegate }) => ok({
|
|
239
248
|
agent_address: agent.address,
|
|
240
|
-
link: setupLink + (services ?? []).map((x) => `&service=${encodeURIComponent(x.id)}:${encodeURIComponent(x.max)}`).join('') + (net && netRoots[cfg.chainId] ? `&target=${netRoots[cfg.chainId]}:${PUBLISH_SELECTOR}` : ''),
|
|
249
|
+
link: setupLink + (services ?? []).map((x) => `&service=${encodeURIComponent(x.id)}:${encodeURIComponent(x.max)}`).join('') + (net && netRoots[cfg.chainId] ? `&target=${netRoots[cfg.chainId]}:${PUBLISH_SELECTOR}` : '') + (delegate ? '&delegate=1' : ''),
|
|
241
250
|
services: services?.length ? services.map((x) => `${x.id}: up to ${x.max} USDG`) : undefined,
|
|
242
251
|
steps: [
|
|
243
252
|
'Open the link. It opens the FIDES app with this agent already filled in.',
|
|
@@ -285,6 +294,51 @@ export function createFidesMcpServer(cfg) {
|
|
|
285
294
|
data: z.string().optional().describe('call: hex calldata, starting with the 4-byte function selector'),
|
|
286
295
|
label: z.string().optional().describe('A short description shown in the plan'),
|
|
287
296
|
});
|
|
297
|
+
/** The tool's actions as Builder actions, in the budget token unless a transfer names another. */
|
|
298
|
+
async function buildActions(actions, budget) {
|
|
299
|
+
const built = [];
|
|
300
|
+
for (const [i, a] of actions.entries()) {
|
|
301
|
+
const n = i + 1;
|
|
302
|
+
if (a.kind === 'transfer') {
|
|
303
|
+
if (!a.to || !isAddress(a.to))
|
|
304
|
+
return { error: `Action ${n}: "to" must be an address.` };
|
|
305
|
+
const asset = a.token ? (isAddress(a.token) ? getAddress(a.token) : null) : budget;
|
|
306
|
+
if (!asset)
|
|
307
|
+
return { error: `Action ${n}: "token" must be an address.` };
|
|
308
|
+
const t = await token(asset);
|
|
309
|
+
let amount;
|
|
310
|
+
try {
|
|
311
|
+
amount = parseUnits(String(a.amount ?? ''), t.decimals);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return { error: `Action ${n}: "amount" must be a number of ${t.symbol}, like "1.5".` };
|
|
315
|
+
}
|
|
316
|
+
if (amount <= 0n)
|
|
317
|
+
return { error: `Action ${n}: "amount" must be more than zero.` };
|
|
318
|
+
built.push({ id: `a${n}`, type: 'TOKEN_TRANSFER', label: a.label ?? `send ${formatUnits(amount, t.decimals)} ${t.symbol} to ${getAddress(a.to)}`, target: asset, spend: { asset, amount },
|
|
319
|
+
payload: encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [getAddress(a.to), amount] }) });
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
if (!a.target || !isAddress(a.target))
|
|
323
|
+
return { error: `Action ${n}: "target" must be a contract address.` };
|
|
324
|
+
if (!a.data || !/^0x([0-9a-fA-F]{2}){4,}$/.test(a.data))
|
|
325
|
+
return { error: `Action ${n}: "data" must be hex calldata starting with the 4-byte selector.` };
|
|
326
|
+
const t = await token(budget);
|
|
327
|
+
let spend = 0n;
|
|
328
|
+
if (a.amount) {
|
|
329
|
+
try {
|
|
330
|
+
spend = parseUnits(a.amount, t.decimals);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return { error: `Action ${n}: "amount" must be a number of ${t.symbol}.` };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
built.push({ id: `a${n}`, type: 'CONTRACT_WRITE', label: a.label ?? `call ${a.data.slice(0, 10)} on ${getAddress(a.target)}`, target: getAddress(a.target), payload: a.data,
|
|
337
|
+
...(spend > 0n ? { spend: { asset: budget, amount: spend } } : {}) });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return { built };
|
|
341
|
+
}
|
|
288
342
|
server.registerTool('fides_plan', {
|
|
289
343
|
title: 'Plan actions under the FIDES',
|
|
290
344
|
description: 'Builds the actions into one execution plan: checks every action against the FIDES, groups what can go out together, and simulates it against the chain. Sends nothing. Returns a plan_id for fides_execute. Show the result to the user before executing.',
|
|
@@ -296,48 +350,10 @@ export function createFidesMcpServer(cfg) {
|
|
|
296
350
|
return fail(notLoaded());
|
|
297
351
|
if (fides.agent.toLowerCase() !== agent.address.toLowerCase())
|
|
298
352
|
return fail(`The loaded FIDES is for agent ${fides.agent}, not this computer's agent key ${agent.address}.`);
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (a.kind === 'transfer') {
|
|
304
|
-
if (!a.to || !isAddress(a.to))
|
|
305
|
-
return fail(`Action ${n}: "to" must be an address.`);
|
|
306
|
-
const asset = a.token ? (isAddress(a.token) ? getAddress(a.token) : null) : budget;
|
|
307
|
-
if (!asset)
|
|
308
|
-
return fail(`Action ${n}: "token" must be an address.`);
|
|
309
|
-
const t = await token(asset);
|
|
310
|
-
let amount;
|
|
311
|
-
try {
|
|
312
|
-
amount = parseUnits(String(a.amount ?? ''), t.decimals);
|
|
313
|
-
}
|
|
314
|
-
catch {
|
|
315
|
-
return fail(`Action ${n}: "amount" must be a number of ${t.symbol}, like "1.5".`);
|
|
316
|
-
}
|
|
317
|
-
if (amount <= 0n)
|
|
318
|
-
return fail(`Action ${n}: "amount" must be more than zero.`);
|
|
319
|
-
built.push({ id: `a${n}`, type: 'TOKEN_TRANSFER', label: a.label ?? `send ${formatUnits(amount, t.decimals)} ${t.symbol} to ${getAddress(a.to)}`, target: asset, spend: { asset, amount },
|
|
320
|
-
payload: encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [getAddress(a.to), amount] }) });
|
|
321
|
-
}
|
|
322
|
-
else {
|
|
323
|
-
if (!a.target || !isAddress(a.target))
|
|
324
|
-
return fail(`Action ${n}: "target" must be a contract address.`);
|
|
325
|
-
if (!a.data || !/^0x([0-9a-fA-F]{2}){4,}$/.test(a.data))
|
|
326
|
-
return fail(`Action ${n}: "data" must be hex calldata starting with the 4-byte selector.`);
|
|
327
|
-
const t = await token(budget);
|
|
328
|
-
let spend = 0n;
|
|
329
|
-
if (a.amount) {
|
|
330
|
-
try {
|
|
331
|
-
spend = parseUnits(a.amount, t.decimals);
|
|
332
|
-
}
|
|
333
|
-
catch {
|
|
334
|
-
return fail(`Action ${n}: "amount" must be a number of ${t.symbol}.`);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
built.push({ id: `a${n}`, type: 'CONTRACT_WRITE', label: a.label ?? `call ${a.data.slice(0, 10)} on ${getAddress(a.target)}`, target: getAddress(a.target), payload: a.data,
|
|
338
|
-
...(spend > 0n ? { spend: { asset: budget, amount: spend } } : {}) });
|
|
339
|
-
}
|
|
340
|
-
}
|
|
353
|
+
const made = await buildActions(actions, fides.spendLimits[0].asset);
|
|
354
|
+
if ('error' in made)
|
|
355
|
+
return fail(made.error);
|
|
356
|
+
const built = made.built;
|
|
341
357
|
const plan = await new FidesBuilder({ client, fides, agent }).add(built).build();
|
|
342
358
|
const savings = plan.estimatedSavings();
|
|
343
359
|
const simulations = savings.estimatedTransactions ? await plan.simulate() : [];
|
|
@@ -583,11 +599,165 @@ export function createFidesMcpServer(cfg) {
|
|
|
583
599
|
return fail('Paid request stopped: ' + failure(err?.shortMessage ?? err?.message ?? err));
|
|
584
600
|
}
|
|
585
601
|
});
|
|
602
|
+
// ---- SUB-FIDES ----
|
|
603
|
+
server.registerTool('fides_delegate', {
|
|
604
|
+
title: 'Hand another agent a slice of the FIDES (SUB-FIDES)',
|
|
605
|
+
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.',
|
|
606
|
+
inputSchema: {
|
|
607
|
+
agent: z.string().describe('The child agent\'s address'),
|
|
608
|
+
amount: z.string().describe('The most the child may spend, in the budget token, e.g. "0.1"'),
|
|
609
|
+
minutes: z.number().int().positive().optional().describe('How long the slice lasts. Defaults to as long as this FIDES; never longer'),
|
|
610
|
+
},
|
|
611
|
+
}, async ({ agent: child, amount, minutes }) => {
|
|
612
|
+
try {
|
|
613
|
+
if (!subClient || !subAddrs)
|
|
614
|
+
return fail(`SUB-FIDES is not deployed on chain ${cfg.chainId}.`);
|
|
615
|
+
const parent = await currentFides();
|
|
616
|
+
if (!parent)
|
|
617
|
+
return fail(notLoaded());
|
|
618
|
+
if (parent.agent.toLowerCase() !== agent.address.toLowerCase())
|
|
619
|
+
return fail(`The loaded FIDES is for agent ${parent.agent}, not this computer's agent key ${agent.address}.`);
|
|
620
|
+
if (!isAddress(child))
|
|
621
|
+
return fail('"agent" must be an address.');
|
|
622
|
+
if (child.toLowerCase() === agent.address.toLowerCase())
|
|
623
|
+
return fail('An agent cannot delegate to itself.');
|
|
624
|
+
const v2 = await accountOf(subClient, parent.owner);
|
|
625
|
+
if (v2.toLowerCase() !== parent.account.toLowerCase())
|
|
626
|
+
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.');
|
|
627
|
+
const asset = parent.spendLimits[0].asset, t = await token(asset);
|
|
628
|
+
let value;
|
|
629
|
+
try {
|
|
630
|
+
value = parseUnits(amount, t.decimals);
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
return fail(`"amount" must be a number of ${t.symbol}, like "0.1".`);
|
|
634
|
+
}
|
|
635
|
+
const expiry = minutes ? BigInt(Math.min(Number(parent.validUntil), Math.floor(Date.now() / 1000) + minutes * 60)) : parent.validUntil;
|
|
636
|
+
const unsigned = SubFidesKit.create(parent, { agent: getAddress(child), spend: [{ asset, amount: value }], expiry });
|
|
637
|
+
const sub = await SubFidesKit.sign(unsigned, agent, subClient, parent);
|
|
638
|
+
if (cfg.api)
|
|
639
|
+
await SubFidesKit.register(cfg.api, parent, sub);
|
|
640
|
+
fs.mkdirSync(subsDir, { recursive: true });
|
|
641
|
+
const file = path.join(subsDir, `${sub.id.slice(0, 18)}.json`);
|
|
642
|
+
fs.writeFileSync(file, toJson({ parent, sub }, 2) + '\n');
|
|
643
|
+
return ok({
|
|
644
|
+
sub_fides: sub.id,
|
|
645
|
+
child_agent: sub.agent,
|
|
646
|
+
budget: `${formatUnits(value, t.decimals)} ${t.symbol}`,
|
|
647
|
+
expires: new Date(Number(sub.validUntil) * 1000).toISOString(),
|
|
648
|
+
contracts: sub.targets.map((r) => r.target),
|
|
649
|
+
file,
|
|
650
|
+
how_the_child_gets_it: cfg.api ? 'Shared with the FIDES API: a child running FIDES (Claude or the SDK) finds it by its agent address. The file is a copy to hand over directly.' : 'Hand the child the file.',
|
|
651
|
+
revoke: 'The owner can revoke it any time in the FIDES app, on this FIDES\'s page.',
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
return fail(err instanceof SubFidesError ? err.message : 'Could not delegate: ' + (err?.shortMessage ?? err?.message ?? err));
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
/** The SUB-FIDES handed to this agent that can still act: from a file, or found through the FIDES API. */
|
|
659
|
+
async function receivedSubs(file) {
|
|
660
|
+
const found = [];
|
|
661
|
+
if (file) {
|
|
662
|
+
const full = file.replace(/^~(?=$|[\\/])/, os.homedir());
|
|
663
|
+
found.push(fromJson(fs.readFileSync(full, 'utf8')));
|
|
664
|
+
}
|
|
665
|
+
if (cfg.directory) {
|
|
666
|
+
try {
|
|
667
|
+
const res = await fetch(`${cfg.directory}/v1/agents/${agent.address}/subs`);
|
|
668
|
+
if (res.ok)
|
|
669
|
+
for (const x of fromJson(await res.text()).subs ?? [])
|
|
670
|
+
if (x.parentStatus?.valid)
|
|
671
|
+
found.push({ parent: x.parent, sub: x.sub });
|
|
672
|
+
}
|
|
673
|
+
catch { /* the API is optional when a file is given */ }
|
|
674
|
+
}
|
|
675
|
+
const now = BigInt(Math.floor(Date.now() / 1000));
|
|
676
|
+
const live = [];
|
|
677
|
+
for (const x of found) {
|
|
678
|
+
if (x.sub.agent.toLowerCase() !== agent.address.toLowerCase() || now >= x.sub.validUntil)
|
|
679
|
+
continue;
|
|
680
|
+
if (subClient && await SubFidesKit.isRevoked(subClient, x.parent, x.sub).catch(() => true))
|
|
681
|
+
continue;
|
|
682
|
+
live.push(x);
|
|
683
|
+
}
|
|
684
|
+
return live.sort((a, b) => Number(b.sub.validAfter - a.sub.validAfter));
|
|
685
|
+
}
|
|
686
|
+
server.registerTool('fides_sub_plan', {
|
|
687
|
+
title: 'Plan actions under a SUB-FIDES handed to this agent',
|
|
688
|
+
description: 'For when another agent handed this agent a SUB-FIDES (a slice of its FIDES). Builds the actions into one plan, checks every action against the slice, and simulates it. Sends nothing. Returns a plan_id for fides_execute, which sends it through the relayer (no gas needed). Show the result to the user before executing.',
|
|
689
|
+
inputSchema: { actions: z.array(actionSchema).min(1).max(50), file: z.string().optional().describe('Path to a SUB-FIDES file, if the user has one. Otherwise it is found by this agent\'s address') },
|
|
690
|
+
}, async ({ actions, file }) => {
|
|
691
|
+
try {
|
|
692
|
+
if (!subClient)
|
|
693
|
+
return fail(`SUB-FIDES is not deployed on chain ${cfg.chainId}.`);
|
|
694
|
+
const [x] = await receivedSubs(file);
|
|
695
|
+
if (!x)
|
|
696
|
+
return fail(`No active SUB-FIDES has been handed to this agent (${agent.address}). The delegating agent creates one with this address, or the user gives you its file.`);
|
|
697
|
+
const made = await buildActions(actions, x.sub.spendLimits[0]?.asset ?? x.parent.spendLimits[0].asset);
|
|
698
|
+
if ('error' in made)
|
|
699
|
+
return fail(made.error);
|
|
700
|
+
const child = new SubAgent({ client: subClient, parent: x.parent, sub: x.sub, agent, ...(cfg.api ? { api: cfg.api } : { relayer: agent }) });
|
|
701
|
+
const p = await child.plan(made.built);
|
|
702
|
+
const sim = p.bundle ? await child.simulate(p) : { success: false, reason: 'NOTHING_TO_SEND' };
|
|
703
|
+
const id = (p.bundleHash ?? '0x').slice(0, 18);
|
|
704
|
+
if (sim.success)
|
|
705
|
+
subPlans.set(id, { agent: child, plan: p, parent: x.parent, sub: x.sub });
|
|
706
|
+
const t = await token(x.sub.spendLimits[0]?.asset ?? x.parent.spendLimits[0].asset);
|
|
707
|
+
return ok({
|
|
708
|
+
plan_id: sim.success ? id : null,
|
|
709
|
+
ready_to_execute: sim.success,
|
|
710
|
+
sub_fides: x.sub.id,
|
|
711
|
+
slice: `${x.sub.spendLimits.map((l) => formatUnits(l.amount, t.decimals) + ' ' + t.symbol).join(', ')} until ${new Date(Number(x.sub.validUntil) * 1000).toISOString()}`,
|
|
712
|
+
delegated_by: x.parent.agent,
|
|
713
|
+
actions: p.plan.actions.map((a) => ({ action: a.label, decision: a.decision === 'AUTHORIZED' ? 'inside the SUB-FIDES' : a.decision === 'REJECTED' ? `blocked: ${REASONS[a.reason ?? ''] ?? a.reason} (${a.reason})` : 'duplicate, sent once' })),
|
|
714
|
+
transactions: p.bundle ? `${p.bundle.calls.length} onchain write(s) go out as 1 transaction` : 'nothing to execute',
|
|
715
|
+
simulation: !p.bundle ? 'nothing to execute: no action is inside the SUB-FIDES' : sim.success ? 'simulated against the chain: it would succeed' : `would fail: ${failure(sim.reason)}`,
|
|
716
|
+
sent: 'nothing has been sent',
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
catch (err) {
|
|
720
|
+
return fail('Could not build the plan: ' + (err?.shortMessage ?? err?.message ?? err));
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
server.registerTool('fides_subs', {
|
|
724
|
+
title: 'SUB-FIDES handed out and received',
|
|
725
|
+
description: 'The SUB-FIDES this agent has handed to other agents, and the ones handed to it that can still act.',
|
|
726
|
+
inputSchema: {},
|
|
727
|
+
}, async () => {
|
|
728
|
+
try {
|
|
729
|
+
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
|
+
const now = BigInt(Math.floor(Date.now() / 1000));
|
|
731
|
+
return ok({
|
|
732
|
+
handed_out: given.map((s) => ({ sub_fides: s.id, child_agent: s.agent, budget: s.spendLimits.map((l) => l.amount.toString()), expires: new Date(Number(s.validUntil) * 1000).toISOString(), expired: now >= s.validUntil })),
|
|
733
|
+
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
|
+
});
|
|
735
|
+
}
|
|
736
|
+
catch (err) {
|
|
737
|
+
return fail('Could not list SUB-FIDES: ' + (err?.shortMessage ?? err?.message ?? err));
|
|
738
|
+
}
|
|
739
|
+
});
|
|
586
740
|
server.registerTool('fides_execute', {
|
|
587
741
|
title: 'Execute a plan',
|
|
588
742
|
description: 'Executes a plan made by fides_plan, by its plan_id. Only the actions inside the FIDES are sent; blocked ones never are. Moves real funds from the FIDES Account: confirm with the user first.',
|
|
589
743
|
inputSchema: { plan_id: z.string() },
|
|
590
744
|
}, async ({ plan_id }) => {
|
|
745
|
+
const subEntry = subPlans.get(plan_id);
|
|
746
|
+
if (subEntry) {
|
|
747
|
+
subPlans.delete(plan_id);
|
|
748
|
+
try {
|
|
749
|
+
const r = await subEntry.agent.execute(subEntry.plan);
|
|
750
|
+
return ok({
|
|
751
|
+
result: r.status === 'EXECUTED' ? 'executed' : 'not executed',
|
|
752
|
+
transactions: r.txHash || r.error ? [{ status: r.status.toLowerCase(), transaction: txLink(r.txHash), ...(r.error ? { reason: failure(r.error) } : {}) }] : [],
|
|
753
|
+
under: `SUB-FIDES ${subEntry.sub.id} from ${subEntry.parent.agent}`,
|
|
754
|
+
blocked_and_not_sent: subEntry.plan.plan.rejected.map((a) => `${a.label}: ${REASONS[a.reason ?? ''] ?? a.reason}`),
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
catch (err) {
|
|
758
|
+
return fail('Execution stopped: ' + failure(err?.shortMessage ?? err?.message ?? err));
|
|
759
|
+
}
|
|
760
|
+
}
|
|
591
761
|
const entry = plans.get(plan_id);
|
|
592
762
|
if (!entry)
|
|
593
763
|
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.6.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",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"viem": "^2.21.0",
|
|
41
41
|
"zod": "^4.6.5",
|
|
42
42
|
"@fidesbuild/x402": "0.2.0",
|
|
43
|
-
"@fidesbuild/net": "0.2.1"
|
|
43
|
+
"@fidesbuild/net": "0.2.1",
|
|
44
|
+
"@fidesbuild/sub": "0.2.0"
|
|
44
45
|
}
|
|
45
46
|
}
|