@metalntwx/metal-agent 0.2.0 → 0.3.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/README.md +31 -27
- package/dist/chain.js +1 -1
- package/dist/commands/mandate.js +450 -138
- package/dist/commands/pay.js +1 -1
- package/dist/help.js +16 -15
- package/dist/index.js +27 -11
- package/dist/keystore.js +4 -8
- package/dist/mcp.js +8 -8
- package/package.json +6 -6
- package/skills/metal-agent/SKILL.md +23 -16
package/dist/commands/pay.js
CHANGED
|
@@ -59,7 +59,7 @@ export async function pay(target, account, url, limit, print, request = {}) {
|
|
|
59
59
|
*/
|
|
60
60
|
export async function payUnderMandate(target, payer, url, print, request = {}) {
|
|
61
61
|
const mandate = await loadMandate(payer.credentialPath);
|
|
62
|
-
const signDigest = await mandateAgentSigner(mandate, payer.
|
|
62
|
+
const signDigest = await mandateAgentSigner(mandate, payer.credentialPath, payer.mandateKeyFile);
|
|
63
63
|
const paying = createMandatePaymentClient({ credential: mandate.credential, signDigest, network: target.network });
|
|
64
64
|
print(`mandate ${paying.reference}`);
|
|
65
65
|
await paidRequest(target, { ...paying, payer: paying.principal }, url, print, request);
|
package/dist/help.js
CHANGED
|
@@ -18,8 +18,8 @@ const COMMANDS = [
|
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
name: "pay",
|
|
21
|
-
usage: `metal-agent pay <url> ${NETWORK} [--key <file>] [--max <usdz>]\n [--method <method>] [--body <text>] [--header 'name: value']...\nmetal-agent pay <url> --mandate <credential> ${NETWORK} [--
|
|
22
|
-
about: "Requests the URL and pays its x402 offer in *USDZ. With --key, the agent pays from its own key and refuses, before signing, any offer above --max (1 *USDZ by default). With --mandate, it pays from the owner's account within the mandate's limits, signed by the
|
|
21
|
+
usage: `metal-agent pay <url> ${NETWORK} [--key <file>] [--max <usdz>]\n [--method <method>] [--body <text>] [--header 'name: value']...\nmetal-agent pay <url> --mandate <credential> ${NETWORK} [--mandate-key-file <file>]`,
|
|
22
|
+
about: "Requests the URL and pays its x402 offer in *USDZ. With --key, the agent pays from its own key and refuses, before signing, any offer above --max (1 *USDZ by default). With --mandate, it pays from the owner's account within the mandate's limits, signed by the .key.json file in the credential's directory whose key the credential names, unless --mandate-key-file names another. --method (GET by default, POST with a --body), --body (application/json unless a --header names a content type) and each --header are sent again with the payment.",
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
name: "status",
|
|
@@ -28,23 +28,23 @@ const COMMANDS = [
|
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
name: "mcp",
|
|
31
|
-
usage: "metal-agent mcp [--key <file>] [--max <usdz>]\nmetal-agent mcp --mandate <credential> [--
|
|
31
|
+
usage: "metal-agent mcp [--key <file>] [--max <usdz>]\nmetal-agent mcp --mandate <credential> [--mandate-key-file <file>]",
|
|
32
32
|
about: "Serves pay, fund and status as MCP tools over stdio. What pay spends is fixed here, by whoever starts the server: its key and --max, or one mandate. The model calling the tools reads pages and offers anyone can write, so it can choose no other key, no mandate and no higher limit; under a key, a max it passes can only lower the server's.",
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
name: "account create",
|
|
36
|
-
usage: `metal-agent account create --out <owner key file
|
|
37
|
-
about: "
|
|
36
|
+
usage: `metal-agent account create --out <owner key file> | --key <owner key file> ${NETWORK}`,
|
|
37
|
+
about: "Creates the owner's Metal account, a smart account the owner's key controls, and prints its address, the --principal of the owner's credentials. The owner's key pays the gas; one that holds none is funded first, from the faucet on testnet. An account that exists is named, and nothing is sent. Keep the owner's key where no agent can read it.",
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
name: "mandate credential create",
|
|
41
|
-
usage: `metal-agent mandate credential create --principal <address> --cap <usdz> --budget <usdz> --out <file>\n --key <owner key file> | --issuer-key <file> ${NETWORK}\n [--payee <address>[,<name>[,<website>]]]... [--expires-in <seconds>] [--max-occurrences <n>]\n [--description <text>] [--
|
|
42
|
-
about: "Writes the limits an agent pays within into a credential file: the most per payment (--cap), the most in total over the mandate's whole life (--budget, which never resets), an expiry (--expires-in, 30 days by default) and, when --payee names any, the addresses it may pay, each with an optional name and website. Amounts are in *USDZ, to the cent. The
|
|
41
|
+
usage: `metal-agent mandate credential create --principal <address> --cap <usdz> --budget <usdz> --out <file>\n --key <owner key file> | --issuer-key <file> ${NETWORK}\n [--payee <address>[,<name>[,<website>]]]... [--expires-in <seconds>] [--starts-in <seconds>] [--max-occurrences <n>]\n [--description <text>] [--mandate-key-file <file>]`,
|
|
42
|
+
about: "Writes the limits an agent pays within into a credential file: the most per payment (--cap), the most in total over the mandate's whole life (--budget, which never resets), an expiry (--expires-in, 30 days by default), a start (--starts-in, at once by default) and, when --payee names any, the addresses it may pay, each with an optional name and website. Amounts are in *USDZ, to the cent. The mandate key the agent signs with is written beside the credential, mandate.key.json for --out mandate.json, unless --mandate-key-file names one that exists, as for a replacement. The keys that sign the credential are the owner's: they live beside the owner's key file, <key>.issuer.json, created on first use, unless --issuer-key names their file.",
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
name: "mandate bind",
|
|
46
46
|
usage: `metal-agent mandate bind --credential <file> --key <owner key file> ${NETWORK} [--issuer <file>]... [--signer-key <file>]...`,
|
|
47
|
-
about: "Binds the credential on the owner's account, which then pays under it. The account's
|
|
47
|
+
about: "Binds the credential on the owner's account, which then pays under it. The account's owners sign: --key pays the gas and is the sole signer unless repeated --signer-key names the owners' keys. --issuer names the issuers whose credentials the owner accepts, the <key>.issuer.public.json beside the owner's key by default.",
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
name: "mandate replace",
|
|
@@ -68,21 +68,22 @@ const COMMANDS = [
|
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
name: "mandate pull",
|
|
71
|
-
usage: `metal-agent mandate pull --credential <file> --to <address> --amount <usdz> ${NETWORK}\n [--key <file>] [--
|
|
72
|
-
about: "Pays an address the mandate allows, with no seller. The
|
|
71
|
+
usage: `metal-agent mandate pull --credential <file> --to <address> --amount <usdz> ${NETWORK}\n [--key <file>] [--mandate-key-file <file>] [--record-refusal]`,
|
|
72
|
+
about: "Pays an address the mandate allows, with no seller. The facilitator submits it and pays its gas, unless --key names a funded key to submit it. A payout the mandate would refuse is reported and not sent, and the command exits 1; with --key, --record-refusal sends it so the refusal is recorded on chain. The outcome is read from the chain: paid, refused, held in the payee's escrow, or pending. A pending pull is kept beside the credential, and running the same command again sends that same pull, which pays at most once.",
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
name: "mandate verify",
|
|
76
76
|
usage: `metal-agent mandate verify --credential <file> --issuer <file>... ${NETWORK} [--tx <hash> [--signed-offer <text>]]`,
|
|
77
|
-
about: "Checks the mandate and every payment under it from the chain alone, or one --tx: the binding, the credential against it and under an --issuer you trust, and each payment as the
|
|
77
|
+
about: "Checks the mandate and every payment under it from the chain alone, or one --tx: the binding, the credential against it and under an --issuer you trust, and each payment as the mandate key's signed per-payment mandate. With --signed-offer, the order the agent signed for that payment, as pay printed it. A check that fails exits 1.",
|
|
78
78
|
},
|
|
79
79
|
];
|
|
80
|
-
const SHARED = `Chain commands also take --rpc-url and --faucet-url,
|
|
81
|
-
|
|
80
|
+
const SHARED = `Chain commands also take --rpc-url and --faucet-url, and mandate pull
|
|
81
|
+
--facilitator-url, which override METAL_RPC_URL, METAL_FAUCET_URL and
|
|
82
|
+
METAL_FACILITATOR_URL, which override each network's defaults.
|
|
82
83
|
On testnet, status, account create and the mandate commands other than
|
|
83
84
|
credential create need an RPC endpoint of your own:
|
|
84
|
-
sign up in the Metal console and create one. account create and
|
|
85
|
-
|
|
85
|
+
sign up in the Metal console and create one. account create and mandate bind,
|
|
86
|
+
replace, renew, revoke, status and verify take --factory <address> when the network's MetalAccountFactory is
|
|
86
87
|
not the one metal-agent knows.
|
|
87
88
|
|
|
88
89
|
metal-agent <command> --help explains one command.`;
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { credentialCreate, accountCreate, mandateBind, mandatePull, mandateRenew
|
|
|
9
9
|
import { parseHeaders, parseMax, pay, payUnderMandate } from "./commands/pay.js";
|
|
10
10
|
import { status } from "./commands/status.js";
|
|
11
11
|
import { helpFor } from "./help.js";
|
|
12
|
-
import { DEFAULT_KEY_PATH,
|
|
12
|
+
import { DEFAULT_KEY_PATH, issuerKeyPath, publicKeyPath, readKey } from "./keystore.js";
|
|
13
13
|
import { mcpAuthority, serveMcp } from "./mcp.js";
|
|
14
14
|
const print = (line) => console.log(line);
|
|
15
15
|
async function main(argv) {
|
|
@@ -20,6 +20,7 @@ async function main(argv) {
|
|
|
20
20
|
network: { type: "string" },
|
|
21
21
|
"rpc-url": { type: "string" },
|
|
22
22
|
"faucet-url": { type: "string" },
|
|
23
|
+
"facilitator-url": { type: "string" },
|
|
23
24
|
key: { type: "string" },
|
|
24
25
|
factory: { type: "string" },
|
|
25
26
|
"signer-key": { type: "string", multiple: true },
|
|
@@ -33,13 +34,14 @@ async function main(argv) {
|
|
|
33
34
|
credential: { type: "string" },
|
|
34
35
|
replaces: { type: "string" },
|
|
35
36
|
issuer: { type: "string", multiple: true },
|
|
36
|
-
"
|
|
37
|
+
"mandate-key-file": { type: "string" },
|
|
37
38
|
"issuer-key": { type: "string" },
|
|
38
39
|
principal: { type: "string" },
|
|
39
40
|
payee: { type: "string", multiple: true },
|
|
40
41
|
cap: { type: "string" },
|
|
41
42
|
budget: { type: "string" },
|
|
42
43
|
"expires-in": { type: "string" },
|
|
44
|
+
"starts-in": { type: "string" },
|
|
43
45
|
"max-occurrences": { type: "string" },
|
|
44
46
|
tx: { type: "string" },
|
|
45
47
|
"signed-offer": { type: "string" },
|
|
@@ -50,6 +52,10 @@ async function main(argv) {
|
|
|
50
52
|
},
|
|
51
53
|
});
|
|
52
54
|
const [command, sub] = positionals;
|
|
55
|
+
const extra = positionals.slice(argumentCount(command, sub));
|
|
56
|
+
if (extra.length > 0) {
|
|
57
|
+
throw new Error(`unexpected argument${extra.length > 1 ? "s" : ""} ${extra.map((a) => JSON.stringify(a)).join(" ")}; quote a value that holds a space`);
|
|
58
|
+
}
|
|
53
59
|
if (values.help || command === undefined || command === "help") {
|
|
54
60
|
print(helpFor(command === "help" ? positionals.slice(1) : positionals.slice(0, 3)));
|
|
55
61
|
return command === undefined && !values.help ? 2 : 0;
|
|
@@ -57,6 +63,7 @@ async function main(argv) {
|
|
|
57
63
|
const on = () => target(values.network, {
|
|
58
64
|
rpcUrl: values["rpc-url"],
|
|
59
65
|
faucetUrl: values["faucet-url"],
|
|
66
|
+
facilitatorUrl: values["facilitator-url"],
|
|
60
67
|
});
|
|
61
68
|
switch (command) {
|
|
62
69
|
case "key":
|
|
@@ -74,12 +81,14 @@ async function main(argv) {
|
|
|
74
81
|
const request = { method: values.method, body: values.body, headers: parseHeaders(values.header, "--header") };
|
|
75
82
|
if (values.mandate !== undefined) {
|
|
76
83
|
if (values.max !== undefined || values.key !== undefined) {
|
|
77
|
-
throw new Error("pay --mandate takes neither --max nor --key: the mandate sets the limit, and the agent
|
|
84
|
+
throw new Error("pay --mandate takes neither --max nor --key: the mandate sets the limit, and the agent signs with its mandate key");
|
|
78
85
|
}
|
|
79
|
-
const payer = { credentialPath: values.mandate,
|
|
86
|
+
const payer = { credentialPath: values.mandate, mandateKeyFile: values["mandate-key-file"] };
|
|
80
87
|
await payUnderMandate(on(), payer, sub, print, request);
|
|
81
88
|
return 0;
|
|
82
89
|
}
|
|
90
|
+
if (values["mandate-key-file"] !== undefined)
|
|
91
|
+
throw new Error("--mandate-key-file goes with --mandate");
|
|
83
92
|
const limit = { maxAmount: parseMax(values.max, "--max"), raise: "pass --max to allow it" };
|
|
84
93
|
await pay(on(), await readKey(values.key ?? DEFAULT_KEY_PATH), sub, limit, print, request);
|
|
85
94
|
return 0;
|
|
@@ -88,7 +97,7 @@ async function main(argv) {
|
|
|
88
97
|
if (sub !== "create")
|
|
89
98
|
throw new Error(`unknown account subcommand "${sub ?? ""}"; try: account create`);
|
|
90
99
|
if ((values.key === undefined) === (values.out === undefined))
|
|
91
|
-
throw new Error("account create needs --out, the owner key file
|
|
100
|
+
throw new Error("account create needs --out, the owner key file, created when it does not exist, or --key, one that exists");
|
|
92
101
|
const factory = accountFactoryFor(on().network, values.factory === undefined ? undefined : parseAddress(values.factory, "--factory"), "pass --factory");
|
|
93
102
|
await accountCreate(on(), { accountFactory: factory, ...(values.key === undefined ? { out: values.out } : { keyPath: values.key }) }, print);
|
|
94
103
|
return 0;
|
|
@@ -116,10 +125,11 @@ async function main(argv) {
|
|
|
116
125
|
cap: parseCents(values.cap, "--cap"),
|
|
117
126
|
budget: parseCents(values.budget, "--budget"),
|
|
118
127
|
expiresIn: parseCount(values["expires-in"], "--expires-in") ?? 30 * 86_400,
|
|
128
|
+
startsIn: parseCount(values["starts-in"], "--starts-in"),
|
|
119
129
|
maxOccurrences: parseCount(values["max-occurrences"], "--max-occurrences"),
|
|
120
130
|
description: values.description ?? "A mandate issued by metal-agent",
|
|
121
131
|
out: need(values.out, "--out"),
|
|
122
|
-
|
|
132
|
+
mandateKeyFile: values["mandate-key-file"],
|
|
123
133
|
// The keys that sign the owner's credentials are as private as the owner's key: they live beside it.
|
|
124
134
|
issuerKeyPath: values["issuer-key"] ?? issuerKeyPath(need(values.key, "--key, the account owner's key file, beside which the keys that sign its credentials live, or --issuer-key")),
|
|
125
135
|
}, print);
|
|
@@ -145,14 +155,12 @@ async function main(argv) {
|
|
|
145
155
|
await mandateStatus(on(), need(values.credential, "--credential"), accountFactory(), print);
|
|
146
156
|
}
|
|
147
157
|
else if (sub === "pull") {
|
|
148
|
-
const credentialPath = need(values.credential, "--credential");
|
|
149
|
-
const agentKey = values["agent-key"] ?? agentKeyPath(credentialPath);
|
|
150
158
|
const args = {
|
|
151
|
-
credentialPath,
|
|
152
|
-
|
|
159
|
+
credentialPath: need(values.credential, "--credential"),
|
|
160
|
+
mandateKeyFile: values["mandate-key-file"],
|
|
153
161
|
to: parseAddress(values.to, "--to"),
|
|
154
162
|
amount: parsePullAmount(values.amount, "--amount"),
|
|
155
|
-
keyPath: values.key
|
|
163
|
+
keyPath: values.key,
|
|
156
164
|
recordRefusal: values["record-refusal"] === true,
|
|
157
165
|
};
|
|
158
166
|
return (await mandatePull(on(), args, print)) === "paid" ? 0 : 1;
|
|
@@ -181,6 +189,14 @@ async function main(argv) {
|
|
|
181
189
|
throw new Error(`unknown command "${command}"\n\n${helpFor([])}`);
|
|
182
190
|
}
|
|
183
191
|
}
|
|
192
|
+
/** How many words a command takes before its options: `key import <file|hex>` and `mandate credential create` three, `mcp` one, the other commands two; any number for `help` and for a command the dispatch then names unknown. */
|
|
193
|
+
function argumentCount(command, sub) {
|
|
194
|
+
if ((command === "key" && sub === "import") || (command === "mandate" && sub === "credential"))
|
|
195
|
+
return 3;
|
|
196
|
+
if (command === "mcp")
|
|
197
|
+
return 1;
|
|
198
|
+
return ["key", "fund", "pay", "account", "mandate", "status"].includes(command ?? "") ? 2 : Infinity;
|
|
199
|
+
}
|
|
184
200
|
main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
|
|
185
201
|
// An endpoint's URL can carry its credential, and this output often lands in an agent's transcript.
|
|
186
202
|
console.error(`error: ${redactUrls(error instanceof Error ? error.message : String(error))}`);
|
package/dist/keystore.js
CHANGED
|
@@ -14,16 +14,12 @@ function beside(path, suffix) {
|
|
|
14
14
|
const ext = extname(path);
|
|
15
15
|
return `${path.slice(0, path.length - ext.length)}.${suffix}${ext}`;
|
|
16
16
|
}
|
|
17
|
-
/** The
|
|
18
|
-
export const
|
|
17
|
+
/** The mandate key `credential create` writes beside the credential at `credentialPath`: `mandate.json` to `mandate.key.json`. */
|
|
18
|
+
export const mandateKeyPath = (credentialPath) => beside(credentialPath, "key");
|
|
19
|
+
/** The directory of pulls `mandate pull` sent without an outcome yet, beside the credential at `credentialPath`: `mandate.json` to `mandate.pulls`. */
|
|
20
|
+
export const pendingPullsPath = (credentialPath) => `${credentialPath.slice(0, credentialPath.length - extname(credentialPath).length)}.pulls`;
|
|
19
21
|
/** The keys that sign the owner's credentials, beside the owner's key file and as private as it: `owner-key.json` to `owner-key.issuer.json`. */
|
|
20
22
|
export const issuerKeyPath = (ownerKeyPath) => beside(ownerKeyPath, "issuer");
|
|
21
|
-
/** The funded key that submits the agent's direct pulls and pays their gas, beside its mandate key: `mandate.agent-key.json` to `mandate.gas-key.json`, `shared.json` to `shared.gas-key.json`. */
|
|
22
|
-
export function gasKeyPath(agentKeyPath) {
|
|
23
|
-
const ext = extname(agentKeyPath);
|
|
24
|
-
const stem = agentKeyPath.slice(0, agentKeyPath.length - ext.length);
|
|
25
|
-
return `${stem.endsWith(".agent-key") ? stem.slice(0, -".agent-key".length) : stem}.gas-key${ext}`;
|
|
26
|
-
}
|
|
27
23
|
/** The secp256k1 key at `path`, generated and written there first when the file does not exist. */
|
|
28
24
|
export async function readOrCreateKey(path) {
|
|
29
25
|
if (await exists(path))
|
package/dist/mcp.js
CHANGED
|
@@ -7,19 +7,19 @@ import { fund } from "./commands/fund.js";
|
|
|
7
7
|
import { parseMax, pay, payUnderMandate } from "./commands/pay.js";
|
|
8
8
|
import { status } from "./commands/status.js";
|
|
9
9
|
import { loadMandate, mandateAgentSigner } from "./commands/mandate.js";
|
|
10
|
-
import { DEFAULT_KEY_PATH,
|
|
10
|
+
import { DEFAULT_KEY_PATH, readKey } from "./keystore.js";
|
|
11
11
|
import { USDZ, formatUsdz } from "@metalntwx/metal-pay";
|
|
12
12
|
import { redactUrls } from "@metalntwx/metal-pay/mandates";
|
|
13
13
|
/** The authority `metal-agent mcp` serves under, from its options. */
|
|
14
14
|
export function mcpAuthority(options) {
|
|
15
15
|
if (options.mandate !== undefined) {
|
|
16
16
|
if (options.key !== undefined || options.max !== undefined) {
|
|
17
|
-
throw new Error("mcp --mandate takes neither --key nor --max: the mandate sets the limit, and the agent
|
|
17
|
+
throw new Error("mcp --mandate takes neither --key nor --max: the mandate sets the limit, and the agent signs with its mandate key");
|
|
18
18
|
}
|
|
19
|
-
return { kind: "mandate", credentialPath: options.mandate,
|
|
19
|
+
return { kind: "mandate", credentialPath: options.mandate, ...(options["mandate-key-file"] !== undefined && { mandateKeyFile: options["mandate-key-file"] }) };
|
|
20
20
|
}
|
|
21
|
-
if (options["
|
|
22
|
-
throw new Error("--
|
|
21
|
+
if (options["mandate-key-file"] !== undefined)
|
|
22
|
+
throw new Error("--mandate-key-file goes with --mandate");
|
|
23
23
|
return { kind: "key", keyPath: options.key ?? DEFAULT_KEY_PATH, maxAmount: parseMax(options.max, "--max") };
|
|
24
24
|
}
|
|
25
25
|
const usdz = (units) => `${formatUsdz(units)} ${USDZ.symbol}`;
|
|
@@ -43,7 +43,7 @@ export function createMcpServer(authority) {
|
|
|
43
43
|
const BUY_RPC = "To buy Metal RPC, POST a JSON-RPC body to the RPC URL; the rpc-aas-endpoint header is the endpoint the payment bought, so send further RPC calls there without paying again.";
|
|
44
44
|
if (authority.kind === "mandate") {
|
|
45
45
|
server.registerTool("pay", {
|
|
46
|
-
description: `Request a URL and pay its x402 challenge in *USDZ on Metal under this server's mandate: the payment comes from the mandate's account, signed by the
|
|
46
|
+
description: `Request a URL and pay its x402 challenge in *USDZ on Metal under this server's mandate: the payment comes from the mandate's account, signed by the mandate key, within the limits its owner set, and a payment outside them is refused with the limit named. Only the owner can change the limits. Returns the settlement transaction hash, the response headers and the body. ${BUY_RPC}`,
|
|
47
47
|
// Strict: a key or limit the model names is refused, not dropped.
|
|
48
48
|
inputSchema: z.object(request).strict(),
|
|
49
49
|
}, ({ url, network: net, method, body, headers }) => collect((print) => payUnderMandate(target(net), authority, url, print, { method, body, headers })));
|
|
@@ -97,10 +97,10 @@ async function collect(run) {
|
|
|
97
97
|
return { content: [{ type: "text", text: lines.join("\n") }], isError: true };
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
-
/** Serves over stdio. A mandate and its
|
|
100
|
+
/** Serves over stdio. A mandate and its mandate key are checked first, so a wrong path fails the host's launch; a key is read when pay needs
|
|
101
101
|
* it, so fund and status work without one. */
|
|
102
102
|
export async function serveMcp(authority) {
|
|
103
103
|
if (authority.kind === "mandate")
|
|
104
|
-
await mandateAgentSigner(await loadMandate(authority.credentialPath), authority.
|
|
104
|
+
await mandateAgentSigner(await loadMandate(authority.credentialPath), authority.credentialPath, authority.mandateKeyFile);
|
|
105
105
|
await createMcpServer(authority).connect(new StdioServerTransport());
|
|
106
106
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metalntwx/metal-agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The `metal-agent` CLI and MCP server: create or import
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "The `metal-agent` CLI and MCP server: create or import the agent's key, fund it on Metal devnet or testnet, pay x402 offers in *USDZ, look up a settlement, and set spending limits for an agent that Metal enforces.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@metalntwx/metal-ts": "^0.3.2",
|
|
22
|
-
"@metalntwx/metal-pay": "^0.
|
|
23
|
-
"@modelcontextprotocol/sdk": "1.
|
|
24
|
-
"@x402/core": "2.
|
|
22
|
+
"@metalntwx/metal-pay": "^0.3.0",
|
|
23
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
24
|
+
"@x402/core": "2.28.0",
|
|
25
25
|
"viem": "^2.54.6",
|
|
26
26
|
"zod": "^3.25.76"
|
|
27
27
|
},
|
|
28
28
|
"dependencyRationale": {
|
|
29
|
-
"@metalntwx/metal-ts": "Funds
|
|
29
|
+
"@metalntwx/metal-ts": "Funds a key on devnet with a MIP20 transfer.",
|
|
30
30
|
"@metalntwx/metal-pay": "The one-call x402 payment clients, mandates, and the Metal network constants.",
|
|
31
31
|
"@modelcontextprotocol/sdk": "The stdio MCP server behind `metal-agent mcp`.",
|
|
32
32
|
"@x402/core": "Decodes the `payment-response` header a paid request returns.",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: metal-agent
|
|
3
|
-
description: "Set up and use the metal-agent CLI to pay x402 sellers on Metal: install it, create or import the agent's
|
|
3
|
+
description: "Set up and use the metal-agent CLI to pay x402 sellers on Metal: install it, create or import the agent's key, fund it, pay a URL that answers 402, pay under a mandate from an account the agent does not own, and look up a settlement. Use when a task needs an agent to pay for an HTTP resource in *USDZ on Metal, to pay with a mandate credential the user gives it, to set up metal-agent, or to check what a Metal payment settled."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
`metal-agent` is the CLI and MCP server of the `@metalntwx/metal-agent` npm
|
|
@@ -71,7 +71,7 @@ Run the steps in order. After each step, show the user its output.
|
|
|
71
71
|
The tools appear after the host reloads. If you cannot reload it, tell
|
|
72
72
|
the user to, and carry on with the CLI.
|
|
73
73
|
|
|
74
|
-
5. Get the agent's
|
|
74
|
+
5. Get the agent's key. Ask the user whether they have a funded key file. If they do,
|
|
75
75
|
ask for its path and run `metal-agent key import <path>`. Otherwise run
|
|
76
76
|
`metal-agent key create`. Tell the user the address. If `key create`
|
|
77
77
|
answers that the key file already holds a key, keep that key and ask the
|
|
@@ -112,12 +112,12 @@ the user; the limits bind you only while it is not.
|
|
|
112
112
|
|
|
113
113
|
## key create
|
|
114
114
|
|
|
115
|
-
Generate the agent's
|
|
115
|
+
Generate the agent's key and write its key file. Prints the address
|
|
116
116
|
and the key file's path. It does not overwrite an existing key file; `--out` writes
|
|
117
117
|
another.
|
|
118
118
|
|
|
119
119
|
```bash
|
|
120
|
-
metal-agent key create --out ./
|
|
120
|
+
metal-agent key create --out ./key.json
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
## key import
|
|
@@ -149,7 +149,7 @@ named network, sign the authorization with the key file, retry, and print
|
|
|
149
149
|
the settlement, the response headers and the body.
|
|
150
150
|
|
|
151
151
|
```bash
|
|
152
|
-
metal-agent pay https://x402-demo-testnet.metalntwx.com/quote --network testnet --key ./
|
|
152
|
+
metal-agent pay https://x402-demo-testnet.metalntwx.com/quote --network testnet --key ./key.json
|
|
153
153
|
```
|
|
154
154
|
|
|
155
155
|
Read `transaction <hash>` from the output for the settlement,
|
|
@@ -208,14 +208,16 @@ when the mandate names them, the payees.
|
|
|
208
208
|
metal-agent pay https://example.com/report --mandate ./mandate.json --network testnet
|
|
209
209
|
```
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
`mandate.
|
|
211
|
+
`pay` signs with the mandate key beside the credential, `mandate.key.json`
|
|
212
|
+
for `mandate.json`; `--mandate-key-file <file>` names it when it is
|
|
213
|
+
elsewhere. Never show the key file's contents. `--max` and `--key` do not
|
|
213
214
|
apply. A payment outside the mandate exits 1 with
|
|
214
215
|
`error: payment refused: <reason> (…)`, a reason starting `mandate_`. Tell
|
|
215
216
|
the user which limit it was; only the owner can change a mandate. After
|
|
216
217
|
`mandate_revoked`, ask the user for the credential that replaced it, and
|
|
217
|
-
pay with that. If the error says
|
|
218
|
-
|
|
218
|
+
pay with that. If the error says no key beside the credential is this
|
|
219
|
+
mandate's, ask the user where the key file is, and pass it as
|
|
220
|
+
`--mandate-key-file`.
|
|
219
221
|
|
|
220
222
|
A seller that asks for your signed order gets it with the payment, and
|
|
221
223
|
`pay` prints it as `signed-offer <text>`. Keep it, and pass it to
|
|
@@ -229,13 +231,18 @@ together.
|
|
|
229
231
|
metal-agent mandate status --credential ./mandate.json --network testnet --rpc-url <endpoint>
|
|
230
232
|
```
|
|
231
233
|
|
|
232
|
-
`mandate pull` pays an address the mandate allows, with no seller. The
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
234
|
+
`mandate pull` pays an address the mandate allows, with no seller. The
|
|
235
|
+
facilitator submits it and pays the gas. The `outcome` line says what
|
|
236
|
+
happened: `paid`; `refused, not sent` with `reason` and `recorded no`, for
|
|
237
|
+
a pull refused when checked, which sends nothing; `refused` with `reason`,
|
|
238
|
+
for one the account refused and recorded; `refused, not paid`, for a sent
|
|
239
|
+
pull that can no longer land; `held`, for one the payee's
|
|
240
|
+
receive policy sent to escrow, so the payee was not paid; or `pending`. Pass
|
|
241
|
+
`--record-refusal`, with `--key` naming a funded key to send it, only when
|
|
242
|
+
the user asks for the refusal to be recorded on chain. On `outcome pending`,
|
|
243
|
+
run the same command again, with the same `--to` and `--amount`: it sends
|
|
244
|
+
the same pull, which pays at most once, or reports how it ended. Never
|
|
245
|
+
change the amount to pay a pending pull again.
|
|
239
246
|
|
|
240
247
|
```bash
|
|
241
248
|
metal-agent mandate pull --credential ./mandate.json --to <payee address> --amount 2.50 --network testnet --rpc-url <endpoint>
|