agentic-wallet-mcp 0.4.0 → 0.4.2

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 CHANGED
@@ -24,7 +24,7 @@ VC issuance → identity proof → pay-per-use).
24
24
  | `prove_identity` | Answer an x401 `PROOF-REQUEST` → return the `PROOF-RESPONSE` header to replay | `{ proofRequest, vc?, revealAttribute?, issuerKeys? }` | `{ proofResponseHeader, verified, presentationId }` |
25
25
  | `pay_and_fetch` | Fetch a URL, auto-pay with x402 (self-pay via Wallet BE) on `402` | `{ url, method?, headers?, body? }` | `{ status, body, paymentMade, amountPaid, amountPaidHuman, asset }` |
26
26
  | `subscribe_and_issue` | Reuse a cached VC if still valid, else pay x402 → MBI issues → return the VC | `{ templateId, attributes, expirationDate?, dryRun?, forceReissue? }` | `{ issued, vcId, vc, txHash, fromCache? }` |
27
- | `create_holder_account` | Onboarding: manually mint an additional/replacement HSM account (the MCP already auto-creates one at startup if `ZETRIX_ADDRESS` is omitted — see Environment below) | `{ password, label?, purpose? }` | `{ zetrixAddress, holderDid, publicKeyHex, message }` |
27
+ | `create_holder_account` | Onboarding: mint an HSM account (the MCP already auto-creates one at startup if `ZETRIX_ADDRESS` is omitted — see Environment below). Always checks for an existing account first — if one is active for this session, returns `{ alreadyExists: true, existing }` without creating anything; pass `confirmNew: true` (after asking the user) to mint a new one anyway | `{ password, label?, purpose?, confirmNew? }` | `{ created, alreadyExists, existing?, zetrixAddress?, holderDid?, publicKeyHex?, message }` |
28
28
 
29
29
  > **VCs are cached locally**, keyed by `templateId`, under `~/.agentic-wallet-mcp/vc-cache/`
30
30
  > (scoped per network + holder — different identities or networks never share a cache).
@@ -39,9 +39,15 @@ VC issuance → identity proof → pay-per-use).
39
39
  > All Ed25519 signing still goes through Wallet BE HSM; no plaintext private keys.
40
40
 
41
41
  > `create_holder_account` mints a **brand-new** keypair — Wallet BE's `/account/create` has no
42
- > way to provision a pre-chosen address. It returns the new `zetrixAddress`/`holderDid` for you
43
- > to paste into `ZETRIX_ADDRESS`/`HSM_PASSWORD` yourself (`HOLDER_DID` is optional see
44
- > Environment below); the tool never writes your MCP config or restarts the server for you.
42
+ > way to provision a pre-chosen address. It always checks first whether an account is already
43
+ > active for this session; if so, it returns `{ alreadyExists: true, existing }` and creates
44
+ > nothing ask the user whether to keep the existing account or replace it, then call again
45
+ > with `confirmNew: true` only if they want a new one. A freshly minted account (address, DID,
46
+ > **and** password) is saved to this MCP's own local account store
47
+ > (`~/.agentic-wallet-mcp/account.json`, owner-only) and reused automatically on the next
48
+ > restart — no manual config edit needed. An explicit `ZETRIX_ADDRESS`/`HSM_PASSWORD` still set
49
+ > in your MCP config always overrides the saved account (see Environment below); the tool never
50
+ > writes the MCP host's own config file or restarts the server for you.
45
51
 
46
52
  > `revealAttribute` on `prove_identity` is optional and usually should stay that way. Omitted,
47
53
  > it's derived automatically from the challenge's DCQL `credential_requirements` — each claim path
@@ -100,9 +106,11 @@ startup, in one of two ways:
100
106
 
101
107
  1. **First-time user — only `HSM_PASSWORD` set.** The MCP creates a brand-new HSM account on
102
108
  Wallet BE (`POST /wallet/hsm/account/create`) and derives the DID from the returned public
103
- key. It logs the new `ZETRIX_ADDRESS` (and `HOLDER_DID`) to stderr on startup copy it into
104
- your MCP config for next time, since nothing is persisted to disk between runs (env vars only
105
- load once, at process start).
109
+ key. It logs the new `ZETRIX_ADDRESS` (and `HOLDER_DID`) to stderr on startup, and saves the
110
+ address, DID, and password to a local account store
111
+ (`~/.agentic-wallet-mcp/account.json`, owner-only) it's reused automatically next run, no
112
+ config edit required. An explicit `ZETRIX_ADDRESS`/`HSM_PASSWORD` set later in your MCP
113
+ config still overrides the saved account.
106
114
  2. **Existing user — `ZETRIX_ADDRESS` + `HSM_PASSWORD` set, `HOLDER_DID` optional.** The MCP
107
115
  always self-signs the address via the existing `POST /wallet/hsm/sign-message` call and
108
116
  derives the DID from the `publicKey` the response carries — no separate lookup endpoint
@@ -171,8 +179,9 @@ the `<...>` placeholders (don't commit a filled copy; `mcp.local.json` is gitign
171
179
  ```
172
180
 
173
181
  > First run, no account yet? Omit `ZETRIX_ADDRESS` (and `HOLDER_DID`) entirely — the MCP creates
174
- > one for you at startup and logs it to stderr; copy it back into `env` for next time. See
175
- > "Onboarding" under Environment above.
182
+ > one for you at startup, logs it to stderr, and saves it (address, DID, password) to
183
+ > `~/.agentic-wallet-mcp/account.json` for automatic reuse next run. See "Onboarding" under
184
+ > Environment above.
176
185
 
177
186
  > Working on this repo locally instead of the published package? Point `command`/`args` at the
178
187
  > local build directly: `"command": "node"`, `"args": ["/absolute/path/to/zetrix-agentic-wallet/dist/server-bundle.cjs"]`.
@@ -190,7 +199,7 @@ the `<...>` placeholders (don't commit a filled copy; `mcp.local.json` is gitign
190
199
  - *"I got a 401 with this PROOF-REQUEST header — prove my identity and give me the PROOF-RESPONSE to replay."* → `prove_identity`
191
200
  - *"Fetch `https://api.example/data` and pay automatically if it asks."* → `pay_and_fetch`
192
201
  - *"Apply for the agent-identity credential with these attributes and pay for it."* → `subscribe_and_issue`
193
- - *"My wallet_status call is failing — I don't have a holder account yet. Set one up."* → `create_holder_account` (asks you for a password, then returns the new address/DID to save)
202
+ - *"My wallet_status call is failing — I don't have a holder account yet. Set one up."* → `create_holder_account` (asks you for a password; if an account already exists it reports that instead of creating — confirm with the user, then re-call with `confirmNew: true` to replace it)
194
203
 
195
204
  For the full ordered script (onboarding → check → issue → prove → pay), see [`docs/USAGE_FLOW.md`](docs/USAGE_FLOW.md).
196
205
 
@@ -200,9 +209,12 @@ Full narrative version with example prompts: [`docs/USAGE_FLOW.md`](docs/USAGE_F
200
209
 
201
210
  **Step 0 — onboarding (once).** Only if `ZETRIX_ADDRESS` isn't set yet: the MCP creates an HSM
202
211
  account automatically at startup from `HSM_PASSWORD` alone (see "Onboarding" under Environment
203
- above) copy the logged `zetrixAddress` into your MCP config for next time. Alternatively, call
204
- `create_holder_account { password }` manually and paste the returned `zetrixAddress` in yourself;
205
- either way, restart the server afterward (env vars load once, at process start).
212
+ above) and saves it locally for automatic reuse. Alternatively, call `create_holder_account
213
+ { password }` manually it always checks for an existing account first and reports it instead
214
+ of creating (pass `confirmNew: true`, after asking the user, to replace it anyway). Either way,
215
+ the account is saved to `~/.agentic-wallet-mcp/account.json` and picked up automatically on the
216
+ next restart; no manual config edit needed unless your MCP config also sets `ZETRIX_ADDRESS`/
217
+ `HSM_PASSWORD` via env, in which case those still take precedence and should be updated too.
206
218
 
207
219
  **Phase 1 — `wallet_status` — pre-check.** Pass any VCs the caller already holds via
208
220
  `heldCredentials`; the response tells you whether the agent-identity credential you need is
@@ -12542,12 +12542,12 @@ __export(index_exports, {
12542
12542
  module.exports = __toCommonJS(index_exports);
12543
12543
  var import_node_crypto2 = require("node:crypto");
12544
12544
  var import_node_os = require("node:os");
12545
- var import_node_path2 = require("node:path");
12545
+ var import_node_path3 = require("node:path");
12546
12546
 
12547
12547
  // package.json
12548
12548
  var package_default = {
12549
12549
  name: "agentic-wallet-mcp",
12550
- version: "0.4.0",
12550
+ version: "0.4.2",
12551
12551
  description: "Agent-facing MCP wallet for Zetrix \u2014 orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
12552
12552
  keywords: ["mcp", "model-context-protocol", "zetrix", "wallet", "x401", "x402", "blockchain"],
12553
12553
  license: "MIT",
@@ -21809,14 +21809,22 @@ var MbiError = class extends Error {
21809
21809
  this.httpStatus = httpStatus;
21810
21810
  }
21811
21811
  };
21812
- var MbiClient = class {
21812
+ var MbiClient = class _MbiClient {
21813
21813
  baseUrl;
21814
21814
  constructor(baseUrl) {
21815
21815
  this.baseUrl = baseUrl.replace(/\/+$/, "");
21816
21816
  }
21817
- /** Phase 1 — POST /v1/vc/pay/apply without X-PAYMENT; expects the 402 challenge. */
21817
+ /**
21818
+ * Phase 1 — POST /v1/vc/pay/apply without X-PAYMENT; expects the 402 challenge.
21819
+ * A free template short-circuits this: MBI issues the VC synchronously and returns
21820
+ * 200 instead, with no phase-2 settle to follow — surfaced via the `issued` field.
21821
+ */
21818
21822
  async applyChallenge(body) {
21819
21823
  const res = await this.fetch("POST", "/v1/vc/pay/apply", body);
21824
+ if (res.status === 200) {
21825
+ const issued = await this.unwrap(res);
21826
+ return { x402Version: 1, accepts: [], issued };
21827
+ }
21820
21828
  if (res.status !== 402) {
21821
21829
  throw await this.error(res, "apply (phase 1) expected 402");
21822
21830
  }
@@ -21864,12 +21872,14 @@ var MbiClient = class {
21864
21872
  const body = await res.json();
21865
21873
  return body.data;
21866
21874
  }
21875
+ static ERROR_BODY_MAX_LEN = 500;
21867
21876
  async error(res, context) {
21868
21877
  const text = await res.text().catch(() => "");
21869
21878
  let msg = text;
21870
21879
  try {
21871
21880
  const j = JSON.parse(text);
21872
- msg = j.message ?? j.error ?? text;
21881
+ const truncated = text.length > _MbiClient.ERROR_BODY_MAX_LEN ? `${text.slice(0, _MbiClient.ERROR_BODY_MAX_LEN)}\u2026 (truncated, ${text.length} bytes total)` : text;
21882
+ msg = `${j.message ?? j.error ?? text} | full body: ${truncated}`;
21873
21883
  } catch {
21874
21884
  }
21875
21885
  return new MbiError(`MBI ${context} \u2014 HTTP ${res.status}: ${msg}`, res.status);
@@ -22064,6 +22074,30 @@ async function subscribeAndIssue(deps, opts) {
22064
22074
  const body = { data, signData, publicKey };
22065
22075
  if (opts.expirationDate) body.expirationDate = opts.expirationDate;
22066
22076
  const challenge = await deps.mbi.applyChallenge(body);
22077
+ if (challenge.issued) {
22078
+ const issued2 = challenge.issued;
22079
+ if (deps.cache) {
22080
+ await deps.cache.set(opts.templateId, {
22081
+ templateId: opts.templateId,
22082
+ vc: issued2.verifiableCredential,
22083
+ vcId: issued2.vcId,
22084
+ txHash: issued2.txHash,
22085
+ paidAsset: "none",
22086
+ amountPaid: "0",
22087
+ issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
22088
+ validUntil: extractValidUntil(issued2.verifiableCredential, opts.expirationDate)
22089
+ });
22090
+ }
22091
+ return {
22092
+ issued: true,
22093
+ vcId: issued2.vcId,
22094
+ vc: issued2.verifiableCredential,
22095
+ txHash: issued2.txHash,
22096
+ paidAsset: "none",
22097
+ amountPaid: "0",
22098
+ ...opts.dryRun ? { reason: "this template requires no payment \u2014 MBI issues synchronously at phase 1, so dryRun could not prevent this issuance" } : {}
22099
+ };
22100
+ }
22067
22101
  const accept = challenge.accepts[0];
22068
22102
  if (!accept) return { issued: false, reason: "MBI 402 returned no payment options" };
22069
22103
  if (opts.dryRun) {
@@ -22114,11 +22148,21 @@ function deriveHolderDid(publicKeyHex) {
22114
22148
  if (hex.length === 76 && hex.slice(0, 4).toLowerCase() === "b001") return `did:zid:${hex.slice(4, 68)}`;
22115
22149
  throw new Error(`onboard: unrecognized public key hex format (length ${hex.length})`);
22116
22150
  }
22117
- async function createHolderAccount(create, input) {
22118
- const { zetrixAddress, publicKeyHex } = await create(input.password, input.label, input.purpose);
22151
+ async function createHolderAccount(deps, input) {
22152
+ const existing = await deps.getExistingAccount();
22153
+ if (existing && !input.confirmNew) {
22154
+ return {
22155
+ created: false,
22156
+ alreadyExists: true,
22157
+ existing,
22158
+ message: `An account already exists for this wallet (zetrixAddress=${existing.zetrixAddress}, holderDid=${existing.holderDid}). Ask the user whether to keep using it or create a brand-new one \u2014 call create_holder_account again with confirmNew:true to mint a new account.`
22159
+ };
22160
+ }
22161
+ const { zetrixAddress, publicKeyHex } = await deps.create(input.password, input.label, input.purpose);
22119
22162
  const holderDid = deriveHolderDid(publicKeyHex);
22120
- const message = `New holder HSM account created. Update your MCP config and restart the server: ZETRIX_ADDRESS=${zetrixAddress}, HSM_PASSWORD=<the password you just provided>. (HOLDER_DID=${holderDid} is optional \u2014 omit it and the MCP re-derives it from the account's public key at startup.)`;
22121
- return { zetrixAddress, holderDid, publicKeyHex, message };
22163
+ await deps.saveAccount({ zetrixAddress, holderDid, hsmPassword: input.password, label: input.label, purpose: input.purpose });
22164
+ const message = `New holder HSM account created \u2014 address, DID, and password saved to the wallet's local account store. Both will be used automatically on the next server restart; no manual config edit needed. If your MCP config also sets ZETRIX_ADDRESS/HSM_PASSWORD via environment variables, update or remove those too: an explicit env ZETRIX_ADDRESS/HSM_PASSWORD always takes precedence over the saved account. ZETRIX_ADDRESS=${zetrixAddress} (HOLDER_DID=${holderDid} is optional \u2014 it re-derives automatically).`;
22165
+ return { created: true, alreadyExists: Boolean(existing), zetrixAddress, holderDid, publicKeyHex, message };
22122
22166
  }
22123
22167
 
22124
22168
  // src/template-aliases.ts
@@ -22187,7 +22231,14 @@ function createTools(deps) {
22187
22231
  return subscribeAndIssue(deps.subscribeDeps, resolved ? { ...input, templateId: resolved } : input);
22188
22232
  },
22189
22233
  create_holder_account(input) {
22190
- return createHolderAccount(deps.createAccount, input);
22234
+ return createHolderAccount(
22235
+ {
22236
+ create: deps.createAccount,
22237
+ getExistingAccount: () => Promise.resolve(deps.config.zetrixAddress ? { zetrixAddress: deps.config.zetrixAddress, holderDid: deps.config.holderDid } : null),
22238
+ saveAccount: deps.saveAccount
22239
+ },
22240
+ input
22241
+ );
22191
22242
  }
22192
22243
  };
22193
22244
  }
@@ -22204,6 +22255,30 @@ async function resolveHolder(deps, input) {
22204
22255
  return { zetrixAddress: input.zetrixAddress, holderDid: derivedDid, created: false, didMismatch };
22205
22256
  }
22206
22257
 
22258
+ // src/clients/account-store.ts
22259
+ var import_promises2 = require("node:fs/promises");
22260
+ var import_node_path2 = require("node:path");
22261
+ function isStoredAccountShape(value) {
22262
+ return typeof value === "object" && value !== null && typeof value.zetrixAddress === "string" && typeof value.holderDid === "string" && typeof value.hsmPassword === "string";
22263
+ }
22264
+ function createFsAccountStore(filePath) {
22265
+ return {
22266
+ async get() {
22267
+ try {
22268
+ const raw = await (0, import_promises2.readFile)(filePath, "utf8");
22269
+ const parsed = JSON.parse(raw);
22270
+ return isStoredAccountShape(parsed) ? parsed : null;
22271
+ } catch {
22272
+ return null;
22273
+ }
22274
+ },
22275
+ async set(account) {
22276
+ await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(filePath), { recursive: true, mode: 448 });
22277
+ await (0, import_promises2.writeFile)(filePath, JSON.stringify(account, null, 2), { encoding: "utf8", mode: 384 });
22278
+ }
22279
+ };
22280
+ }
22281
+
22207
22282
  // src/index.ts
22208
22283
  var packageVersion = package_default.version;
22209
22284
  function buildToolList() {
@@ -22282,13 +22357,17 @@ function buildToolList() {
22282
22357
  },
22283
22358
  {
22284
22359
  name: "create_holder_account",
22285
- description: "Create a new holder HSM account on Wallet BE (onboarding, when ZETRIX_ADDRESS is not yet provisioned). Ask the user for a password first \u2014 never invent one. Returns the new address/DID for the user to save into their MCP config and restart the server; this tool does not persist anything itself.",
22360
+ description: "Create a new holder HSM account on Wallet BE (onboarding). Ask the user for a password first \u2014 never invent one. ALWAYS check first: if an account already exists for this session, this returns { alreadyExists: true, existing: {...} } WITHOUT creating anything \u2014 ask the user whether to keep using the existing account or create a new one, then call again with confirmNew:true only if they choose new. A freshly created account is saved to this MCP's local account store and reused automatically on the next restart; an explicit ZETRIX_ADDRESS in the MCP config still overrides it.",
22286
22361
  inputSchema: {
22287
22362
  type: "object",
22288
22363
  properties: {
22289
22364
  password: { type: "string", description: "HSM password to protect the new account. Must come from the user." },
22290
22365
  label: { type: "string" },
22291
- purpose: { type: "string" }
22366
+ purpose: { type: "string" },
22367
+ confirmNew: {
22368
+ type: "boolean",
22369
+ description: "Set true to mint a new account even though one already exists for this session \u2014 only after the user has confirmed they want a new one."
22370
+ }
22292
22371
  },
22293
22372
  required: ["password"]
22294
22373
  }
@@ -22300,7 +22379,13 @@ function asPayRequest(accept) {
22300
22379
  return { ...accept, extra: { gasModel: "client", ...extra } };
22301
22380
  }
22302
22381
  async function main() {
22303
- const config2 = loadConfig(process.env);
22382
+ const accountStore = createFsAccountStore((0, import_node_path3.join)((0, import_node_os.homedir)(), ".agentic-wallet-mcp", "account.json"));
22383
+ const storedAccount = process.env.ZETRIX_ADDRESS ? null : await accountStore.get();
22384
+ const env = { ...process.env };
22385
+ if (!env.ZETRIX_ADDRESS && storedAccount) env.ZETRIX_ADDRESS = storedAccount.zetrixAddress;
22386
+ if (!env.HOLDER_DID && storedAccount) env.HOLDER_DID = storedAccount.holderDid;
22387
+ if (!env.HSM_PASSWORD && storedAccount) env.HSM_PASSWORD = storedAccount.hsmPassword;
22388
+ const config2 = loadConfig(env);
22304
22389
  const hsmPassword = config2.hsmPassword;
22305
22390
  const be = new WalletBeClient(config2.walletBeUrl);
22306
22391
  const { zetrixAddress, holderDid, created, didMismatch } = await resolveHolder(
@@ -22311,8 +22396,14 @@ async function main() {
22311
22396
  { zetrixAddress: config2.zetrixAddress, holderDid: config2.holderDid, hsmPassword }
22312
22397
  );
22313
22398
  if (created) {
22399
+ await accountStore.set({ zetrixAddress, holderDid, hsmPassword, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
22400
+ process.stderr.write(
22401
+ `agentic-wallet-mcp: no ZETRIX_ADDRESS was set \u2014 created a new HSM account and saved it (address, DID, and password) to ~/.agentic-wallet-mcp/account.json; it will be reused automatically next run. ZETRIX_ADDRESS=${zetrixAddress} (HOLDER_DID=${holderDid} is optional; it re-derives automatically).
22402
+ `
22403
+ );
22404
+ } else if (storedAccount && config2.zetrixAddress === storedAccount.zetrixAddress) {
22314
22405
  process.stderr.write(
22315
- `agentic-wallet-mcp: no ZETRIX_ADDRESS was set \u2014 created a new HSM account. Save this for next run: ZETRIX_ADDRESS=${zetrixAddress} (HOLDER_DID=${holderDid} is optional; it re-derives automatically).
22406
+ `agentic-wallet-mcp: using the holder account saved in ~/.agentic-wallet-mcp/account.json (ZETRIX_ADDRESS=${zetrixAddress}) \u2014 no ZETRIX_ADDRESS/HSM_PASSWORD was set in the MCP config.
22316
22407
  `
22317
22408
  );
22318
22409
  }
@@ -22333,7 +22424,7 @@ async function main() {
22333
22424
  const nodeMetaQuery = (url) => fetch(url, { headers: { Accept: "application/json" } }).then((r) => r.json());
22334
22425
  const resolveTemplateFields = (templateId) => fetchTemplateFields(templateId, config2.templateRegistryAddress, nodeBaseUrl, nodeMetaQuery);
22335
22426
  const cacheScope = (0, import_node_crypto2.createHash)("sha256").update(`${config2.network}:${zetrixAddress}`).digest("hex");
22336
- const vcCache = createFsVcCache((0, import_node_path2.join)((0, import_node_os.homedir)(), ".agentic-wallet-mcp", "vc-cache", cacheScope));
22427
+ const vcCache = createFsVcCache((0, import_node_path3.join)((0, import_node_os.homedir)(), ".agentic-wallet-mcp", "vc-cache", cacheScope));
22337
22428
  const pay = (accept) => {
22338
22429
  assertWithinPaymentCap(accept, config2.maxPaymentAmount);
22339
22430
  return import_x402_zetrix_client.PaymentEngine.pay(asPayRequest(accept), walletCfg, node, {}, walletBeSignerFn);
@@ -22378,6 +22469,7 @@ async function main() {
22378
22469
  payer,
22379
22470
  subscribeDeps: { mbi, sign: subscribeSign, pay, resolveSymbol, holderDid, resolveTemplateFields, cache: vcCache },
22380
22471
  createAccount: (password, label, purpose) => be.createAccount(password, label, purpose),
22472
+ saveAccount: (account) => accountStore.set({ ...account, createdAt: (/* @__PURE__ */ new Date()).toISOString() }),
22381
22473
  cache: vcCache
22382
22474
  };
22383
22475
  const tools = createTools(deps);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-wallet-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Agent-facing MCP wallet for Zetrix — orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
5
5
  "keywords": ["mcp", "model-context-protocol", "zetrix", "wallet", "x401", "x402", "blockchain"],
6
6
  "license": "MIT",