@aifinpay/mcp 2.0.0-rc.5 → 2.1.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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -64
  3. package/bin/aifinpay-mcp.js +115 -67
  4. package/dist/api.d.ts +2 -0
  5. package/dist/api.d.ts.map +1 -0
  6. package/dist/api.js +6 -0
  7. package/dist/api.js.map +1 -0
  8. package/dist/config.d.ts +11 -2
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +27 -11
  11. package/dist/config.js.map +1 -1
  12. package/dist/identity.d.ts +9 -0
  13. package/dist/identity.d.ts.map +1 -0
  14. package/dist/identity.js +81 -0
  15. package/dist/identity.js.map +1 -0
  16. package/dist/index.d.ts +3 -6
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +3 -6
  19. package/dist/index.js.map +1 -1
  20. package/dist/safe-fetch.d.ts.map +1 -1
  21. package/dist/safe-fetch.js +73 -19
  22. package/dist/safe-fetch.js.map +1 -1
  23. package/dist/server.d.ts +1 -1
  24. package/dist/server.d.ts.map +1 -1
  25. package/dist/server.js +108 -22
  26. package/dist/server.js.map +1 -1
  27. package/dist/tools/agent-address.d.ts.map +1 -1
  28. package/dist/tools/agent-address.js +5 -4
  29. package/dist/tools/agent-address.js.map +1 -1
  30. package/dist/tools/{payable-fetch.d.ts → agent-history.d.ts} +21 -23
  31. package/dist/tools/agent-history.d.ts.map +1 -0
  32. package/dist/tools/agent-history.js +141 -0
  33. package/dist/tools/agent-history.js.map +1 -0
  34. package/dist/tools/agent-quota.d.ts.map +1 -1
  35. package/dist/tools/agent-quota.js +3 -1
  36. package/dist/tools/agent-quota.js.map +1 -1
  37. package/dist/tools/{agent-quote.d.ts → dev-payment-quote.d.ts} +10 -12
  38. package/dist/tools/dev-payment-quote.d.ts.map +1 -0
  39. package/dist/tools/dev-payment-quote.js +80 -0
  40. package/dist/tools/dev-payment-quote.js.map +1 -0
  41. package/dist/tools/production-control.d.ts +104 -0
  42. package/dist/tools/production-control.d.ts.map +1 -1
  43. package/dist/tools/production-control.js +177 -14
  44. package/dist/tools/production-control.js.map +1 -1
  45. package/dist/tools/supported-chains.d.ts +38 -0
  46. package/dist/tools/supported-chains.d.ts.map +1 -0
  47. package/dist/tools/supported-chains.js +132 -0
  48. package/dist/tools/supported-chains.js.map +1 -0
  49. package/package.json +28 -6
  50. package/skills/SKILL.md +271 -0
  51. package/smithery.yaml +137 -0
  52. package/dist/directory.d.ts +0 -55
  53. package/dist/directory.d.ts.map +0 -1
  54. package/dist/directory.js +0 -56
  55. package/dist/directory.js.map +0 -1
  56. package/dist/tools/agent-call.d.ts +0 -68
  57. package/dist/tools/agent-call.d.ts.map +0 -1
  58. package/dist/tools/agent-call.js +0 -129
  59. package/dist/tools/agent-call.js.map +0 -1
  60. package/dist/tools/agent-claim-self.d.ts +0 -66
  61. package/dist/tools/agent-claim-self.d.ts.map +0 -1
  62. package/dist/tools/agent-claim-self.js +0 -299
  63. package/dist/tools/agent-claim-self.js.map +0 -1
  64. package/dist/tools/agent-quote.d.ts.map +0 -1
  65. package/dist/tools/agent-quote.js +0 -81
  66. package/dist/tools/agent-quote.js.map +0 -1
  67. package/dist/tools/pay-with-split.d.ts +0 -93
  68. package/dist/tools/pay-with-split.d.ts.map +0 -1
  69. package/dist/tools/pay-with-split.js +0 -138
  70. package/dist/tools/pay-with-split.js.map +0 -1
  71. package/dist/tools/payable-fetch.d.ts.map +0 -1
  72. package/dist/tools/payable-fetch.js +0 -135
  73. package/dist/tools/payable-fetch.js.map +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AiFinPay
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,105 +1,158 @@
1
1
  # @aifinpay/mcp
2
2
 
3
- MCP server exposing AiFinPay payment and quote primitives to MCP-aware
4
- agent runtimes. Canonical domain: **aifinpay.io**.
3
+ AiFinPay MCP server for persistent agent identity, Agent Passport resolution,
4
+ route discovery and non-signing settlement invoices. Canonical domain:
5
+ **aifinpay.io**.
5
6
 
6
- AIFP-1 is gross-inclusive: payer total equals the quote, merchant receives 99%,
7
- AiFinPay receives 1%, creator/referral receives 0%. AIFP-2/x402 currently
8
- charges 0% at the protocol layer. Fund-moving paths fail closed until the exact
9
- deployment, runtime hash, governance profile, merchant target, asset and paid
10
- E2E evidence are verified.
7
+ Stable `2.0.0` release. This release does not expose payment-signing tools. Returning a
8
+ wallet address or preparing an invoice does not authorize or execute a payment.
9
+ Signing remains gated on the verified SDK v2 executor and its release evidence.
10
+ Local MCP tools for wallet discovery, payment history, quotas and non-signing
11
+ payment preparation. Read the bundled [skill](skills/SKILL.md), also exposed
12
+ as the MCP resource `aifinpay://skill`.
11
13
 
12
14
  ## Tools
13
15
 
14
- | Tool | What it does |
15
- |---|---|
16
- | `payable_fetch(url, opts?)` | Fetch any URL. On 402, auto-detect facilitator, sign, retry. |
17
- | `agent_address()` | Return the agent's funding addresses on **both** chains — Polygon `0x…` (default settlement: io.net, Exa, Venice bridges) and Solana base58 (Seat PDA / leaderboard). One seed, two chains — fund either. |
18
- | `agent_quote(url)` | Inspect a 402 challenge without paying. Shows the merchant's quoted amount + facilitator flavor. |
19
- | `agent_call(provider, …)` | Call a live provider from the AiFinPay directory (io.net, Exa, Venice, …) with automatic payment. |
20
- | `pay_with_split(…)` | Retired compatibility tool. Returns `legacy_split_route_retired`; never creates an invoice or moves funds. |
21
- | `quote_split(…)` | Retired compatibility tool. Use a canonical AIFP-1 quote. |
22
- | `agent_claim_self(magic_link_url)` | Link this agent to your dashboard account via a one-shot magic link from `aifinpay.io/login` — spend history shows up in the dashboard. |
16
+ | Tool | Purpose |
17
+ | ------------------------ | -------------------------------------------------------- |
18
+ | `agent_address` | Read the current Solana, EVM and Casper addresses. |
19
+ | `agent_reload` | Reload local wallet files in the current MCP connection. |
20
+ | `agent_quota` | Read the agent's quota. |
21
+ | `agent_passport_resolve` | Resolve the global Agent Passport identity. |
22
+ | `settlement_routes` | Read the available verified settlement routes. |
23
+ | `settlement_invoice` | Prepare a non-signing settlement invoice. |
23
24
 
24
- ## Install
25
+ Legacy `payable_fetch`, `agent_call`, `agent_quote`, `pay_with_split`,
26
+ `quote_split` and `agent_claim_self` tools are not registered by this RC.
27
+
28
+ ## Persistent wallet selection
29
+
30
+ The CLI (`init` and stdio), programmatic server and `agent_reload` use the same
31
+ priority:
32
+
33
+ 1. `SEED_HASH`: an existing 32-byte seed encoded as 64 hex characters, optionally
34
+ prefixed with `0x`. It is passed directly to `AiFinPayAgent.fromSeed`; do not
35
+ hash it again or supply a mnemonic.
36
+ 2. `./aifinpay/agents.json`, relative to the MCP process's working directory.
37
+ Set an absolute `AIFINPAY_AGENTS_FILE` when a desktop client's working
38
+ directory differs from the project directory. The file contains an `agents`
39
+ array of records with `id` and private `seed_hash` fields. If there is more
40
+ than one record, select exactly one with `AIFINPAY_AGENT_ID`.
41
+ 3. `AIFINPAY_AGENT_SECRET`: a legacy base58 Solana secret.
42
+ 4. `~/.aifinpay/agent.json`, or `AIFINPAY_HOME/agent.json`: the legacy CLI
43
+ keystore. Encrypted files require `AIFINPAY_WALLET_PASSPHRASE`.
44
+
45
+ Keep seeds and wallet files private and out of chat, source control and logs.
46
+ Invalid or ambiguous configured inputs fail; they never generate a replacement
47
+ wallet. With no configured wallet at all the server has an ephemeral identity:
48
+ **do not fund it**.
49
+
50
+ ## Initialize and connect
51
+
52
+ This release does not register payment-signing tools. Its legacy SDK dependency
53
+ supplies wallet derivation only; installing this MCP does not enable v2
54
+ settlement. An invoice or quote is not a completed payment.
55
+
56
+ ## Local configuration
25
57
 
26
58
  ```bash
27
- # Globally — usable as `npx @aifinpay/mcp` from any client config
28
- # (installs the latest stable — the old @alpha tag is retired, don't use it)
29
- npm install -g @aifinpay/mcp
59
+ npx @aifinpay/mcp init
30
60
  ```
31
61
 
32
- ## Use with Claude Desktop
62
+ Select the wallet in this order: `SEED_HASH` → `./aifinpay/agents.json` →
63
+ legacy `AIFINPAY_AGENT_SECRET` → `~/.aifinpay/agent.json`.
64
+ SEED_HASH means a 32-byte hex seed, passed directly to SDK `fromSeed`.
65
+ See the skill for the exact project-file schema and multi-agent selection.
66
+ `init` prints the selected wallet's public addresses. If a seed or project wallet
67
+ is already configured, it does not create a second legacy wallet. With no
68
+ wallet, it creates the legacy keystore with mode `600`; existing keystores are
69
+ retained. Back up that file privately. Use the published `2.0.0` release or
70
+ build this source checkout.
33
71
 
34
- Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
72
+ Use an absolute `AIFINPAY_AGENTS_FILE` path when the host's working directory
73
+ is not your project directory. Do not put the seed itself in a shared config.
74
+ A client configuration can use the keystore without embedding its secret:
35
75
 
36
76
  ```json
37
77
  {
38
78
  "mcpServers": {
39
79
  "aifinpay": {
40
80
  "command": "npx",
41
- "args": ["@aifinpay/mcp"],
81
+ "args": ["-y", "@aifinpay/mcp@2.0.0"],
42
82
  "env": {
43
- "AIFINPAY_AGENT_SECRET": "<base58 secret — see below>",
44
- "AIFINPAY_MAX_USD": "0.50"
83
+ "AIFINPAY_AGENTS_FILE": "/absolute/project/aifinpay/agents.json",
84
+ "AIFINPAY_AGENT_ID": "research-agent"
45
85
  }
46
86
  }
47
87
  }
48
88
  }
49
89
  ```
50
90
 
51
- Restart Claude Desktop. Now Claude can call `payable_fetch`, `agent_address`,
52
- and `agent_quote` like any other tool.
91
+ The release above must be published before this npx command can install it.
92
+ For a source checkout, run `npm ci && npm run build` in mcp/ and configure
93
+ `node /absolute/path/to/mcp/bin/aifinpay-mcp.js` instead.
53
94
 
54
- ## First run — generating an agent
95
+ If using the legacy keystore, run `npx @aifinpay/mcp init` once and omit the
96
+ project-file variables. Optional `AIFINPAY_WALLET_PASSPHRASE` encrypts that
97
+ legacy file at creation. An already connected server can load it with
98
+ `agent_reload`; it does not need a new conversation. A changed package or
99
+ launch environment requires a server reconnect, subject to host support.
55
100
 
56
- If `AIFINPAY_AGENT_SECRET` is not set, the server generates an ephemeral
57
- keypair and **prints it to stderr** at startup:
101
+ ## History
58
102
 
59
- ```
60
- [warn] no AIFINPAY_AGENT_SECRET set — generated EPHEMERAL agent.
61
- address: 9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
62
- secret: 4xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
63
- >> Save this secret to AIFINPAY_AGENT_SECRET to keep the agent across restarts.
64
- ```
65
-
66
- Save the secret to `AIFINPAY_AGENT_SECRET` in your client config so the
67
- agent identity (and any funded Seat) persists across restarts.
103
+ Connect the MCP server once. After `init` or a wallet file update, call
104
+ `agent_reload` in that same connection; no new conversation is required. A
105
+ failed reload keeps the previous wallet. Changed shell environment variables
106
+ require reconnecting the MCP process, since its environment is a startup
107
+ snapshot. Verify `agent_address` against the wallet you intend to use.
68
108
 
69
- ## Environment variables
109
+ ## Other environment variables
70
110
 
71
- | Var | Default | Purpose |
72
- |---|---|---|
73
- | `AIFINPAY_AGENT_SECRET` | — | Base58 secret. If absent → ephemeral agent printed to stderr. |
74
- | `AIFINPAY_BASE_URL` | `https://aifinpay.io` | Backend URL for nonce + funding probes. |
75
- | `AIFINPAY_TIMEOUT_MS` | `30000` | Request timeout. |
76
- | `AIFINPAY_MAX_USD` | — | Mandatory positive finite operator ceiling for any fund-moving tool. |
111
+ | Variable | Default | Purpose |
112
+ | ---------------------------- | --------------------- | ------------------------------------------------------------------------------------ |
113
+ | `AIFINPAY_BASE_URL` | `https://aifinpay.io` | Backend URL. |
114
+ | `AIFINPAY_TIMEOUT_MS` | `30000` | Request timeout. |
115
+ | `AIFINPAY_MAX_USD` | — | Configures the underlying agent's per-call cap; it does not enable signing. |
116
+ | `AIFINPAY_GATEWAY_ORIGINS` | SDK default | Comma-separated exact HTTPS origins trusted for AIFP-1 settlement. |
117
+ | `AIFINPAY_GATEWAY_PATH_MODE` | `gateway` | Use `gateway` for merchant-slug identity or `direct` for full request-path identity. |
77
118
 
78
- ## Programmatic use
119
+ `agent_history({address, source:"transactions"})` reads indexed Polygon
120
+ settlements; `source:"receipts"` reads retained batches, including test-mode
121
+ payments. Pass `passport` instead of, or alongside, `address` to resolve a
122
+ public passport first. A backend without the passport resolver cannot satisfy
123
+ passport-only queries. No secret API key is needed for public metadata.
79
124
 
80
- ```ts
81
- import { createServer, loadConfigFromEnv } from "@aifinpay/mcp";
82
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
125
+ Responses do not include bearer receipts. Limits are 1..100; follow
126
+ `next_offset`. History source/coverage is explicit: it is not a full wallet
127
+ explorer. Dev networks currently use receipt history, not the Polygon ledger.
83
128
 
84
- const { server } = await createServer({
85
- ...loadConfigFromEnv(),
86
- agentSecretB58: "your-secret-here",
87
- maxAmountUsd: 0.10,
88
- });
129
+ ```
130
+ const { server } = await createServer(loadConfigFromEnv());
89
131
  await server.connect(new StdioServerTransport());
90
132
  ```
91
133
 
92
- ## How `payable_fetch` works
134
+ The programmatic equivalents of identity environment options are `seedHash`,
135
+ `agentsFile`, `agentId`, `agentSecretB58`, `walletHome` and `walletPassphrase`.
136
+
137
+ - AIFINPAY_BASE_URL: backend origin; default https://aifinpay.io.
138
+ - AIFINPAY_HOME: directory of the legacy agent.json keystore.
139
+ - AIFINPAY_MODE=dev: expose dev_payment_quote; requires a separate dev base URL.
140
+
141
+ - AIFINPAY_TIMEOUT_MS: SDK request timeout.
142
+ - AIFINPAY_MAX_USD: legacy per-call budget; does not enable signing.
143
+ - AIFINPAY_TRUSTED_HOSTS: exact hosts allowed to bypass the DNS pre-check.
144
+ - AIFINPAY_ALLOW_PRIVATE_FETCH=1: explicit local-development network access.
145
+ Never enable this on the public hosted MCP.
93
146
 
94
- 1. Sends the request unauthenticated.
95
- 2. On `402`, the underlying [`@aifinpay/agent`](../node) SDK detects the
96
- facilitator flavor (AiFinPay native, Coinbase x402, …).
97
- 3. Signs a payment payload and retries.
98
- 4. Returns `{ status, ok, headers, body }` to the agent.
147
+ For a self-hosted AIFP-1 gateway, set `AIFINPAY_GATEWAY_ORIGINS` to exact HTTPS
148
+ origins. `AIFINPAY_GATEWAY_PATH_MODE=direct` makes the process use each request's
149
+ full pathname as its AIFP-1 resource identity; `gateway` uses the merchant slug.
150
+ The mode is process-wide, does not broaden the trusted-origin allowlist, and
151
+ does not enable the retired signing tools in the production RC.
99
152
 
100
- The flow is identical to calling `agent.pay(url)` directly — this
101
- package just wraps it as an MCP tool surface so LLM agents can call it
102
- without writing payment code.
153
+ Dev quoting does not bypass wallet signatures, issuer verification or
154
+ receipt metering. See the bundled skill for backend prerequisites and the
155
+ remaining Amoy executor requirement.
103
156
 
104
157
  ## License
105
158
 
@@ -22,12 +22,25 @@
22
22
  *
23
23
  * Configure via env (all optional; the secret is read from the keystore below
24
24
  * when the variable is unset):
25
- * AIFINPAY_AGENT_SECRET base58 secret — overrides the keystore
25
+ * SEED_HASH 32-byte hex seed — highest priority
26
+ * AIFINPAY_AGENTS_FILE project agents file — second priority
27
+ * AIFINPAY_AGENT_SECRET legacy base58 secret — overrides the legacy keystore
26
28
  * AIFINPAY_BASE_URL default https://aifinpay.io
27
29
  * AIFINPAY_TIMEOUT_MS default 30000
28
30
  * AIFINPAY_MAX_USD hard cap per single payment (no default)
29
31
  */
30
- import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync, chmodSync } from "node:fs";
32
+ import {
33
+ readFileSync,
34
+ writeFileSync,
35
+ mkdirSync,
36
+ existsSync,
37
+ openSync,
38
+ closeSync,
39
+ fchmodSync,
40
+ fsyncSync,
41
+ linkSync,
42
+ unlinkSync,
43
+ } from "node:fs";
31
44
  import { join } from "node:path";
32
45
  import { homedir } from "node:os";
33
46
  import { createRequire } from "node:module";
@@ -62,7 +75,7 @@ Run init once. It writes ${KEYSTORE} with mode 600 and the server picks
62
75
  it up automatically — you do not have to put the secret in a config file.
63
76
 
64
77
  Env (all optional):
65
- AIFINPAY_AGENT_SECRET base58 secret; overrides the keystore
78
+ SEED_HASH 32-byte hex seed; highest priority\n AIFINPAY_AGENTS_FILE default ./aifinpay/agents.json; second priority\n AIFINPAY_AGENT_ID select one record when the file has multiple agents\n AIFINPAY_AGENT_SECRET legacy base58 secret; after project wallet sources
66
79
  AIFINPAY_MAX_USD hard cap per single payment — set this
67
80
  AIFINPAY_BASE_URL default https://aifinpay.io
68
81
  AIFINPAY_TIMEOUT_MS default 30000
@@ -88,27 +101,50 @@ const PASSPHRASE = process.env.AIFINPAY_WALLET_PASSPHRASE || null;
88
101
  // is unaffected: no passphrase => plaintext, exactly as before. scrypt to
89
102
  // stretch, AES-256-GCM so tampering is detected rather than decrypting to junk.
90
103
  function encryptSecret(secretB58) {
91
- const salt = randomBytes(16), iv = randomBytes(12);
92
- const key = scryptSync(PASSPHRASE, salt, 32, { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
104
+ const salt = randomBytes(16),
105
+ iv = randomBytes(12);
106
+ const key = scryptSync(PASSPHRASE, salt, 32, {
107
+ N: 1 << 15,
108
+ r: 8,
109
+ p: 1,
110
+ maxmem: 64 * 1024 * 1024,
111
+ });
93
112
  const cipher = createCipheriv("aes-256-gcm", key, iv);
94
113
  const ct = Buffer.concat([cipher.update(secretB58, "utf8"), cipher.final()]);
95
- return { enc: "scrypt-aes-256-gcm", salt: salt.toString("base64"), iv: iv.toString("base64"),
96
- tag: cipher.getAuthTag().toString("base64"), ct: ct.toString("base64"),
97
- created: new Date().toISOString() };
114
+ return {
115
+ enc: "scrypt-aes-256-gcm",
116
+ salt: salt.toString("base64"),
117
+ iv: iv.toString("base64"),
118
+ tag: cipher.getAuthTag().toString("base64"),
119
+ ct: ct.toString("base64"),
120
+ created: new Date().toISOString(),
121
+ };
98
122
  }
99
123
  function decryptSecret(store) {
100
124
  if (!PASSPHRASE) throw new Error(`${KEYSTORE} is encrypted but AIFINPAY_WALLET_PASSPHRASE is not set.`);
101
- const key = scryptSync(PASSPHRASE, Buffer.from(store.salt, "base64"), 32, { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
125
+ const key = scryptSync(PASSPHRASE, Buffer.from(store.salt, "base64"), 32, {
126
+ N: 1 << 15,
127
+ r: 8,
128
+ p: 1,
129
+ maxmem: 64 * 1024 * 1024,
130
+ });
102
131
  const dc = createDecipheriv("aes-256-gcm", key, Buffer.from(store.iv, "base64"));
103
132
  dc.setAuthTag(Buffer.from(store.tag, "base64"));
104
- try { return Buffer.concat([dc.update(Buffer.from(store.ct, "base64")), dc.final()]).toString("utf8"); }
105
- catch { throw new Error(`could not decrypt ${KEYSTORE}: wrong AIFINPAY_WALLET_PASSPHRASE or the file was modified.`); }
133
+ try {
134
+ return Buffer.concat([dc.update(Buffer.from(store.ct, "base64")), dc.final()]).toString("utf8");
135
+ } catch {
136
+ throw new Error(`could not decrypt ${KEYSTORE}: wrong AIFINPAY_WALLET_PASSPHRASE or the file was modified.`);
137
+ }
106
138
  }
107
139
 
108
140
  function readKeystore() {
109
141
  if (!existsSync(KEYSTORE)) return null;
110
142
  let parsed;
111
- try { parsed = JSON.parse(readFileSync(KEYSTORE, "utf8")); } catch { return null; }
143
+ try {
144
+ parsed = JSON.parse(readFileSync(KEYSTORE, "utf8"));
145
+ } catch {
146
+ return null;
147
+ }
112
148
  // A decrypt failure THROWS rather than returning null — null reads as "no
113
149
  // wallet" and mints a new one, which is how a mistyped passphrase loses a key.
114
150
  if (parsed?.enc) return { secretB58: decryptSecret(parsed), created: parsed.created, encrypted: true };
@@ -118,47 +154,82 @@ function readKeystore() {
118
154
  if (arg === "init") {
119
155
  const { Agent, AiFinPayAgent } = await import("@aifinpay/agent");
120
156
 
157
+ const { loadConfigFromEnv } = await import("../dist/config.js");
158
+ const { loadWalletIdentity } = await import("../dist/identity.js");
159
+ const selected = loadWalletIdentity(loadConfigFromEnv());
160
+ if (selected && selected.source !== "legacy-keystore") {
161
+ const agent = selected.seedHash
162
+ ? await AiFinPayAgent.fromSeed(selected.seedHash)
163
+ : await AiFinPayAgent.fromSolanaSecret(selected.secretB58);
164
+ process.stdout.write(
165
+ `Using ${selected.source}; no replacement wallet created.\n` +
166
+ `EVM ${agent.evmAddress}\nSolana ${agent.solanaAddress}\nCasper ${agent.casperAddress}\n` +
167
+ `Keep your configured seed backed up privately. Call agent_reload in an already connected MCP server.\n`
168
+ );
169
+ process.exit(0);
170
+ }
171
+
121
172
  let store = readKeystore();
122
- const freshlyCreated = !store;
123
- if (store) {
124
- // Never silently overwrite. The file is the only copy of a key that may
125
- // already hold funds; a second `init` that regenerated it would destroy a
126
- // wallet to save one line of output.
127
- process.stdout.write(`Existing wallet found at ${KEYSTORE} — keeping it.\n\n`);
128
- } else {
173
+ let freshlyCreated = false;
174
+ if (!store) {
129
175
  mkdirSync(HOME, { recursive: true, mode: 0o700 });
130
176
  const secretB58 = Agent.new().secretB58;
131
- store = { secretB58, created: new Date().toISOString() };
132
- const onDisk = PASSPHRASE ? encryptSecret(secretB58) : store;
133
- writeFileSync(KEYSTORE, JSON.stringify(onDisk, null, 2) + "\n", { mode: 0o600 });
134
- chmodSync(KEYSTORE, 0o600); // writeFileSync honours umask; this does not
177
+ const candidate = { secretB58, created: new Date().toISOString() };
178
+ const onDisk = PASSPHRASE ? encryptSecret(secretB58) : candidate;
179
+ const temporary = join(HOME, `.agent-init-${process.pid}-${randomBytes(12).toString("hex")}.tmp`);
180
+ // Publish a complete file without replacing an existing wallet. Two init
181
+ // processes may both see no keystore; only one exclusive link can win.
182
+ // A direct exclusive write would expose a partially written file to readers.
183
+ let fd;
184
+ let ownsTemporary = false;
185
+ try {
186
+ fd = openSync(temporary, "wx", 0o600);
187
+ ownsTemporary = true;
188
+ fchmodSync(fd, 0o600);
189
+ writeFileSync(fd, JSON.stringify(onDisk, null, 2) + "\n");
190
+ fsyncSync(fd);
191
+ closeSync(fd);
192
+ fd = undefined;
193
+ try {
194
+ linkSync(temporary, KEYSTORE);
195
+ store = candidate;
196
+ freshlyCreated = true;
197
+ } catch (error) {
198
+ if (error.code !== "EEXIST") throw error;
199
+ store = readKeystore();
200
+ if (!store) throw new Error(`Existing ${KEYSTORE} is unreadable; refusing to replace it.`);
201
+ }
202
+ } finally {
203
+ if (fd !== undefined) closeSync(fd);
204
+ if (ownsTemporary) unlinkSync(temporary);
205
+ }
206
+ }
207
+ if (freshlyCreated) {
135
208
  process.stdout.write(
136
209
  PASSPHRASE
137
210
  ? `Created ${KEYSTORE} (mode 600, ENCRYPTED). Keep AIFINPAY_WALLET_PASSPHRASE — the wallet is unrecoverable without it.\n\n`
138
- : `Created ${KEYSTORE} (mode 600, plaintext). For at-rest encryption, set AIFINPAY_WALLET_PASSPHRASE before init.\n\n`,
211
+ : `Created ${KEYSTORE} (mode 600, plaintext). For at-rest encryption, set AIFINPAY_WALLET_PASSPHRASE before init.\n\n`
139
212
  );
213
+ } else {
214
+ process.stdout.write(`Existing wallet found at ${KEYSTORE} — keeping it.\n\n`);
140
215
  }
141
216
 
142
217
  // The one-time recovery output.
143
218
  //
144
- // Shown ONLY on creation, ONLY in the terminal, and ONLY when the keystore is
145
- // plaintext. This is NOT the leak the audit (AIFINP-220 §3) forbids — that is
146
- // a key written to CHAT or LOGS, where an LLM provider or a log shipper keeps
147
- // it forever. This is the deliberate one-time backup prompt every wallet CLI
148
- // shows, on a channel the user controls. Opposite things: the owner reading
149
- // their own recovery phrase once, vs a secret leaking into a transcript.
219
+ // Shown only to the successful creator of a plaintext wallet on a TTY.
220
+ // Piped output (including agent subprocess logs) contains public data only.
150
221
  //
151
222
  // Encrypted keystores print nothing here: recovery there IS the keystore file
152
223
  // plus the passphrase, and re-printing the plaintext secret would defeat the
153
224
  // encryption the user just asked for.
154
- if (freshlyCreated && !PASSPHRASE) {
225
+ if (freshlyCreated && !PASSPHRASE && process.stdout.isTTY) {
155
226
  process.stdout.write(
156
227
  `\n RECOVERY KEY (shown once, never again):\n\n` +
157
- ` ${store.secretB58}\n\n` +
158
- ` Save this off this machine NOW. Anyone with it controls the wallet and\n` +
159
- ` its funds. Do NOT paste it into a chat, an issue, or a config file — the\n` +
160
- ` keystore at ${KEYSTORE} already holds it (encrypt it with\n` +
161
- ` AIFINPAY_WALLET_PASSPHRASE). This line is your OFF-machine backup.\n\n`,
228
+ ` ${store.secretB58}\n\n` +
229
+ ` Save this off this machine NOW. Anyone with it controls the wallet and\n` +
230
+ ` its funds. Do NOT paste it into a chat, an issue, or a config file — the\n` +
231
+ ` keystore at ${KEYSTORE} already holds it (encrypt it with\n` +
232
+ ` AIFINPAY_WALLET_PASSPHRASE). This line is your OFF-machine backup.\n\n`
162
233
  );
163
234
  }
164
235
 
@@ -169,26 +240,26 @@ if (arg === "init") {
169
240
  ` EVM ${agent.evmAddress}\n` +
170
241
  ` Solana ${agent.solanaAddress}\n` +
171
242
  ` Casper ${agent.casperAddress}\n\n` +
172
- `Add this to your MCP client config and restart it:\n\n` +
243
+ `Add this to your MCP client config and connect/reconnect this MCP server:\n\n` +
173
244
  JSON.stringify(
174
245
  {
175
246
  mcpServers: {
176
247
  aifinpay: {
177
248
  command: "npx",
178
- args: ["-y", "@aifinpay/mcp"],
249
+ args: ["-y", `@aifinpay/mcp@${VERSION}`],
179
250
  env: { AIFINPAY_MAX_USD: "0.10" },
180
251
  },
181
252
  },
182
253
  },
183
254
  null,
184
- 2,
255
+ 2
185
256
  ) +
186
257
  `\n\nThe secret is NOT in that block on purpose — the server reads the\n` +
187
- `keystore. Config files get pasted into chats and committed to git.\n\n` +
258
+ `keystore. If this server is already connected, call agent_reload after init.\nA new conversation is not required by this server.\n\n` +
188
259
  `Back up ${KEYSTORE}. It is the only copy. The derivation is not\n` +
189
260
  `BIP-39, so no standard wallet can recover this from a phrase.\n\n` +
190
- `The addresses hold nothing yet. Send POL to the EVM address to let the\n` +
191
- `agent pay for calls.\n`,
261
+ `The EVM address is used for Polygon payments. Check its balance before\n` +
262
+ `funding it or paying for calls.\n`
192
263
  );
193
264
  process.exit(0);
194
265
  }
@@ -200,31 +271,8 @@ if (arg && !arg.startsWith("-")) {
200
271
 
201
272
  // ── server ────────────────────────────────────────────────────────────────
202
273
 
203
- // Fall back to the keystore so `init` once is genuinely enough. Env still wins:
204
- // a caller that sets AIFINPAY_AGENT_SECRET explicitly means it.
205
- if (!process.env.AIFINPAY_AGENT_SECRET) {
206
- const store = readKeystore();
207
- if (store) {
208
- try {
209
- const mode = statSync(KEYSTORE).mode & 0o777;
210
- if (mode & 0o077) {
211
- process.stderr.write(
212
- `[warn] [aifinpay-mcp] ${KEYSTORE} is mode ${mode.toString(8)} — readable beyond your user. ` +
213
- `Run: chmod 600 ${KEYSTORE}\n`,
214
- );
215
- }
216
- } catch {
217
- /* stat failure is not a reason to refuse to start */
218
- }
219
- process.env.AIFINPAY_AGENT_SECRET = store.secretB58;
220
- } else {
221
- process.stderr.write(
222
- `[info] [aifinpay-mcp] no wallet configured. Run \`npx @aifinpay/mcp init\` once ` +
223
- `for a persistent one; starting with a throwaway identity for now.\n`,
224
- );
225
- }
226
- }
227
-
274
+ // Identity selection is shared with the programmatic server and agent_reload.
275
+ // Do not copy a file secret into process.env: it would mask later file updates.
228
276
  const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
229
277
  const { createServer, loadConfigFromEnv } = await import("../dist/index.js");
230
278
 
package/dist/api.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function apiUrl(base: string, path: string): string;
2
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAGzD"}
package/dist/api.js ADDED
@@ -0,0 +1,6 @@
1
+ export function apiUrl(base, path) {
2
+ if (new URL(base).hostname === "api.aifinpay.io" && path.startsWith("/api/"))
3
+ path = path.slice(4);
4
+ return `${base.replace(/\/+$/, "")}${path}`;
5
+ }
6
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,IAAY;IAC/C,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,KAAK,iBAAiB,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnG,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC;AAC9C,CAAC"}
package/dist/config.d.ts CHANGED
@@ -1,8 +1,15 @@
1
1
  /** Runtime configuration loaded from env. */
2
2
  export interface McpConfig {
3
- /** Base58 secret to load the agent identity. If absent, a fresh keypair is
4
- * generated AND printed to stderr at startup with a "save this!" warning. */
3
+ /** Legacy base58 secret, after SEED_HASH and the project agents file. */
5
4
  agentSecretB58?: string;
5
+ /** 32-byte hex seed, consumed exactly as AiFinPayAgent.fromSeed does. */
6
+ seedHash?: string;
7
+ agentsFile?: string;
8
+ agentId?: string;
9
+ walletHome?: string;
10
+ walletPassphrase?: string;
11
+ /** Enables dev discovery/quoting only; never enables settlement signing. */
12
+ devMode?: boolean;
6
13
  /** Custom AiFinPay backend URL. Defaults to production. */
7
14
  baseUrl?: string;
8
15
  /** Request timeout in ms. */
@@ -22,6 +29,8 @@ export interface McpConfig {
22
29
  * unrecognised host means paying whoever answered. Naming a host here is a
23
30
  * statement that you know who that is. */
24
31
  gatewayOrigins?: string[];
32
+ /** AIFP-1 resource identity: merchant slug or full direct request path. */
33
+ gatewayPathMode?: "gateway" | "direct";
25
34
  /** Hosts whose DNS pre-check is skipped, exact names only.
26
35
  *
27
36
  * safe-fetch resolves a hostname and refuses if any answer is a private
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,MAAM,WAAW,SAAS;IACxB;kFAC8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;;;;;;+CAW2C;IAC3C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAE1B;;;;;;;;;8CAS0C;IAC1C,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAExB,qDAAqD;IACrD,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACjE;AAED,wBAAgB,iBAAiB,IAAI,SAAS,CAa7C"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,MAAM,WAAW,SAAS;IACxB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4EAA4E;IAC5E,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;;;;;;+CAW2C;IAC3C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAE1B,2EAA2E;IAC3E,eAAe,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAEvC;;;;;;;;;8CAS0C;IAC1C,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAExB,qDAAqD;IACrD,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACjE;AAED,wBAAgB,iBAAiB,IAAI,SAAS,CAmB7C"}