@aifinpay/mcp 2.0.0-rc.5 → 2.2.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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -69
  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 +17 -2
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +66 -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/payment-state.d.ts +35 -0
  21. package/dist/payment-state.d.ts.map +1 -0
  22. package/dist/payment-state.js +127 -0
  23. package/dist/payment-state.js.map +1 -0
  24. package/dist/safe-fetch.d.ts.map +1 -1
  25. package/dist/safe-fetch.js +73 -19
  26. package/dist/safe-fetch.js.map +1 -1
  27. package/dist/server.d.ts +5 -15
  28. package/dist/server.d.ts.map +1 -1
  29. package/dist/server.js +127 -38
  30. package/dist/server.js.map +1 -1
  31. package/dist/tools/agent-address.d.ts.map +1 -1
  32. package/dist/tools/agent-address.js +5 -4
  33. package/dist/tools/agent-address.js.map +1 -1
  34. package/dist/tools/agent-history.d.ts +58 -0
  35. package/dist/tools/agent-history.d.ts.map +1 -0
  36. package/dist/tools/agent-history.js +141 -0
  37. package/dist/tools/agent-history.js.map +1 -0
  38. package/dist/tools/agent-quota.d.ts.map +1 -1
  39. package/dist/tools/agent-quota.js +3 -1
  40. package/dist/tools/agent-quota.js.map +1 -1
  41. package/dist/tools/{agent-quote.d.ts → dev-payment-quote.d.ts} +10 -12
  42. package/dist/tools/dev-payment-quote.d.ts.map +1 -0
  43. package/dist/tools/dev-payment-quote.js +80 -0
  44. package/dist/tools/dev-payment-quote.js.map +1 -0
  45. package/dist/tools/payable-fetch.d.ts +3 -29
  46. package/dist/tools/payable-fetch.d.ts.map +1 -1
  47. package/dist/tools/payable-fetch.js +231 -109
  48. package/dist/tools/payable-fetch.js.map +1 -1
  49. package/dist/tools/production-control.d.ts +104 -0
  50. package/dist/tools/production-control.d.ts.map +1 -1
  51. package/dist/tools/production-control.js +177 -14
  52. package/dist/tools/production-control.js.map +1 -1
  53. package/dist/tools/supported-chains.d.ts +38 -0
  54. package/dist/tools/supported-chains.d.ts.map +1 -0
  55. package/dist/tools/supported-chains.js +132 -0
  56. package/dist/tools/supported-chains.js.map +1 -0
  57. package/package.json +30 -7
  58. package/skills/SKILL.md +332 -0
  59. package/smithery.yaml +137 -0
  60. package/dist/directory.d.ts +0 -55
  61. package/dist/directory.d.ts.map +0 -1
  62. package/dist/directory.js +0 -56
  63. package/dist/directory.js.map +0 -1
  64. package/dist/tools/agent-call.d.ts +0 -68
  65. package/dist/tools/agent-call.d.ts.map +0 -1
  66. package/dist/tools/agent-call.js +0 -129
  67. package/dist/tools/agent-call.js.map +0 -1
  68. package/dist/tools/agent-claim-self.d.ts +0 -66
  69. package/dist/tools/agent-claim-self.d.ts.map +0 -1
  70. package/dist/tools/agent-claim-self.js +0 -299
  71. package/dist/tools/agent-claim-self.js.map +0 -1
  72. package/dist/tools/agent-quote.d.ts.map +0 -1
  73. package/dist/tools/agent-quote.js +0 -81
  74. package/dist/tools/agent-quote.js.map +0 -1
  75. package/dist/tools/pay-with-split.d.ts +0 -93
  76. package/dist/tools/pay-with-split.d.ts.map +0 -1
  77. package/dist/tools/pay-with-split.js +0 -138
  78. package/dist/tools/pay-with-split.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,197 @@
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
+ Source candidate **2.2.0**, paired with Node SDK **2.1.0**. The published
8
+ MCP 2.1.0 remains read-only; publish the coordinated dependencies before using
9
+ this candidate through npm. Source builds support owner-enabled native Polygon
10
+ v1.4 purchases with `payable_fetch`. Without payment configuration, the server
11
+ keeps its inspection-only tool inventory.
12
+
13
+ ## Enable native paid GET requests
14
+
15
+ Use a persistent wallet created with public `npx @aifinpay/mcp init` or an
16
+ existing configured identity. Keep its passphrase private. The owner must set:
17
+
18
+ ```json
19
+ {
20
+ "AIFINPAY_PAYMENTS_ENABLED": "1",
21
+ "AIFINPAY_GATEWAY_ORIGINS": "https://merchant.example",
22
+ "AIFINPAY_GATEWAY_PATH_MODE": "direct",
23
+ "AIFINPAY_MAX_USD": "0.12",
24
+ "AIFINPAY_DAILY_USD": "1.00",
25
+ "AIFINPAY_MAX_GAS_POL": "0.05"
26
+ }
27
+ ```
28
+
29
+ Limits are examples, not authorization. USD limits cover purchases; gas has its
30
+ own per-transaction POL cap. Origins are exact HTTPS origins authorized by the
31
+ owner. `direct` uses the full path for self-hosted sites; `gateway` uses the
32
+ merchant slug on the hosted gateway. Restart/reconnect the MCP process after
33
+ changing its environment.
34
+
35
+ Call `payable_fetch({"url":"https://merchant.example/api/data"})`. The tool
36
+ uses the original signed v1.4 quote, independent fresh POL/USD pricing, SDK
37
+ runtime/signer verification and owner limits. It persists the prepared transaction
38
+ before sending, verifies the receipt, and reuses the purchased batch. No special
39
+ merchant script, alternate contract or facilitator fallback is used. GET, native
40
+ POL, Polygon live mode only; Amoy payment receipts and stable-token execution are
41
+ not part of this candidate.
42
+
43
+ Private recovery state lives at `AIFINPAY_HOME/payments/<evm-address>/state.json`
44
+ (default home `~/.aifinpay`). State is mode600 in mode700 directories, atomically
45
+ written and fsynced. One operation lock serializes the wallet across processes.
46
+ A retry recovers a pending receipt before allowing another purchase. A process
47
+ crash leaves the lock for owner reconciliation; it is never stolen automatically.
48
+ If a prepared transaction was never broadcast or reverted, reconcile its exact
49
+ hash before clearing anything. Never put raw transactions, receipt JWTs or the
50
+ state file in chat. Unconfirmed payments consume the budget conservatively.
11
51
 
12
52
  ## Tools
13
53
 
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. |
54
+ | Tool | Purpose |
55
+ | ------------------------ | -------------------------------------------------------- |
56
+ | `agent_address` | Read the current Solana, EVM and Casper addresses. |
57
+ | `agent_reload` | Reload local wallet files in the current MCP connection. |
58
+ | `agent_quota` | Read the agent's quota. |
59
+ | `agent_passport_resolve` | Resolve the global Agent Passport identity. |
60
+ | `settlement_routes` | Read the available verified settlement routes. |
61
+ | `settlement_invoice` | Prepare a non-signing settlement invoice. |
62
+
63
+ `payable_fetch` appears only with valid owner payment configuration and a persistent wallet.
64
+ Legacy `agent_call`, `agent_quote`, `pay_with_split`, `quote_split` and
65
+ `agent_claim_self` remain unregistered.
66
+
67
+ ## Persistent wallet selection
68
+
69
+ The CLI (`init` and stdio), programmatic server and `agent_reload` use the same
70
+ priority:
71
+
72
+ 1. `SEED_HASH`: an existing 32-byte seed encoded as 64 hex characters, optionally
73
+ prefixed with `0x`. It is passed directly to `AiFinPayAgent.fromSeed`; do not
74
+ hash it again or supply a mnemonic.
75
+ 2. `./aifinpay/agents.json`, relative to the MCP process's working directory.
76
+ Set an absolute `AIFINPAY_AGENTS_FILE` when a desktop client's working
77
+ directory differs from the project directory. The file contains an `agents`
78
+ array of records with `id` and private `seed_hash` fields. If there is more
79
+ than one record, select exactly one with `AIFINPAY_AGENT_ID`.
80
+ 3. `AIFINPAY_AGENT_SECRET`: a legacy base58 Solana secret.
81
+ 4. `~/.aifinpay/agent.json`, or `AIFINPAY_HOME/agent.json`: the legacy CLI
82
+ keystore. Encrypted files require `AIFINPAY_WALLET_PASSPHRASE`.
23
83
 
24
- ## Install
84
+ Keep seeds and wallet files private and out of chat, source control and logs.
85
+ Invalid or ambiguous configured inputs fail; they never generate a replacement
86
+ wallet. With no configured wallet at all the server has an ephemeral identity:
87
+ **do not fund it**.
88
+
89
+ ## Initialize and connect
90
+
91
+ A quote or invoice is not a completed payment. Enable the owner-configured
92
+ `payable_fetch` tool only after installing the coordinated candidate dependencies.
93
+
94
+ ## Local configuration
25
95
 
26
96
  ```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
97
+ npx @aifinpay/mcp init
30
98
  ```
31
99
 
32
- ## Use with Claude Desktop
100
+ Select the wallet in this order: `SEED_HASH` → `./aifinpay/agents.json` →
101
+ legacy `AIFINPAY_AGENT_SECRET` → `~/.aifinpay/agent.json`.
102
+ SEED_HASH means a 32-byte hex seed, passed directly to SDK `fromSeed`.
103
+ See the skill for the exact project-file schema and multi-agent selection.
104
+ `init` prints the selected wallet's public addresses. If a seed or project wallet
105
+ is already configured, it does not create a second legacy wallet. With no
106
+ wallet, it creates the legacy keystore with mode `600`; existing keystores are
107
+ retained. Back up that file privately. Use the published release or
108
+ build this source checkout.
33
109
 
34
- Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
110
+ Use an absolute `AIFINPAY_AGENTS_FILE` path when the host's working directory
111
+ is not your project directory. Do not put the seed itself in a shared config.
112
+ A client configuration can use the keystore without embedding its secret:
35
113
 
36
114
  ```json
37
115
  {
38
116
  "mcpServers": {
39
117
  "aifinpay": {
40
118
  "command": "npx",
41
- "args": ["@aifinpay/mcp"],
119
+ "args": ["-y", "@aifinpay/mcp@2.2.0"],
42
120
  "env": {
43
- "AIFINPAY_AGENT_SECRET": "<base58 secret — see below>",
44
- "AIFINPAY_MAX_USD": "0.50"
121
+ "AIFINPAY_AGENTS_FILE": "/absolute/project/aifinpay/agents.json",
122
+ "AIFINPAY_AGENT_ID": "research-agent"
45
123
  }
46
124
  }
47
125
  }
48
126
  }
49
127
  ```
50
128
 
51
- Restart Claude Desktop. Now Claude can call `payable_fetch`, `agent_address`,
52
- and `agent_quote` like any other tool.
53
-
54
- ## First run — generating an agent
55
-
56
- If `AIFINPAY_AGENT_SECRET` is not set, the server generates an ephemeral
57
- keypair and **prints it to stderr** at startup:
129
+ The release above must be published before this npx command can install it.
130
+ After the pinned dependencies are published, source checkouts can run
131
+ `npm ci && npm run build` in mcp/ and configure
132
+ `node /absolute/path/to/mcp/bin/aifinpay-mcp.js` instead.
133
+
134
+ If using the legacy keystore, run `npx @aifinpay/mcp init` once and omit the
135
+ project-file variables. Optional `AIFINPAY_WALLET_PASSPHRASE` encrypts that
136
+ legacy file at creation. An already connected server can load it with
137
+ `agent_reload`; it does not need a new conversation. A changed package or
138
+ launch environment requires a server reconnect, subject to host support.
139
+
140
+ ## History
141
+
142
+ Connect the MCP server once. After `init` or a wallet file update, call
143
+ `agent_reload` in that same connection; no new conversation is required. A
144
+ failed reload keeps the previous wallet. Changed shell environment variables
145
+ require reconnecting the MCP process, since its environment is a startup
146
+ snapshot. Verify `agent_address` against the wallet you intend to use.
147
+
148
+ ## Other environment variables
149
+
150
+ | Variable | Default | Purpose |
151
+ | ---------------------------- | --------------------- | ------------------------------------------------------------------------------------ |
152
+ | `AIFINPAY_BASE_URL` | `https://aifinpay.io` | Backend URL. |
153
+ | `AIFINPAY_TIMEOUT_MS` | `30000` | Request timeout. |
154
+ | `AIFINPAY_MAX_USD` | — | Configures the underlying agent's per-call cap; it does not enable signing. |
155
+ | `AIFINPAY_GATEWAY_ORIGINS` | SDK default | Comma-separated exact HTTPS origins trusted for AIFP-1 settlement. |
156
+ | `AIFINPAY_GATEWAY_PATH_MODE` | `gateway` | Use `gateway` for merchant-slug identity or `direct` for full request-path identity. |
157
+
158
+ `agent_history({address, source:"transactions"})` reads indexed Polygon
159
+ settlements; `source:"receipts"` reads retained batches, including test-mode
160
+ payments. Pass `passport` instead of, or alongside, `address` to resolve a
161
+ public passport first. A backend without the passport resolver cannot satisfy
162
+ passport-only queries. No secret API key is needed for public metadata.
163
+
164
+ Responses do not include bearer receipts. Limits are 1..100; follow
165
+ `next_offset`. History source/coverage is explicit: it is not a full wallet
166
+ explorer. Dev networks currently use receipt history, not the Polygon ledger.
58
167
 
59
168
  ```
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.
169
+ const { server } = await createServer(loadConfigFromEnv());
170
+ await server.connect(new StdioServerTransport());
64
171
  ```
65
172
 
66
- Save the secret to `AIFINPAY_AGENT_SECRET` in your client config so the
67
- agent identity (and any funded Seat) persists across restarts.
173
+ The programmatic equivalents of identity environment options are `seedHash`,
174
+ `agentsFile`, `agentId`, `agentSecretB58`, `walletHome` and `walletPassphrase`.
68
175
 
69
- ## Environment variables
70
-
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. |
77
-
78
- ## Programmatic use
79
-
80
- ```ts
81
- import { createServer, loadConfigFromEnv } from "@aifinpay/mcp";
82
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
83
-
84
- const { server } = await createServer({
85
- ...loadConfigFromEnv(),
86
- agentSecretB58: "your-secret-here",
87
- maxAmountUsd: 0.10,
88
- });
89
- await server.connect(new StdioServerTransport());
90
- ```
176
+ - AIFINPAY_BASE_URL: backend origin; default https://aifinpay.io.
177
+ - AIFINPAY_HOME: directory of the legacy agent.json keystore.
178
+ - AIFINPAY_MODE=dev: expose dev_payment_quote; requires a separate dev base URL.
91
179
 
92
- ## How `payable_fetch` works
180
+ - AIFINPAY_TIMEOUT_MS: SDK request timeout.
181
+ - AIFINPAY_MAX_USD: legacy per-call budget; does not enable signing.
182
+ - AIFINPAY_TRUSTED_HOSTS: exact hosts allowed to bypass the DNS pre-check.
183
+ - AIFINPAY_ALLOW_PRIVATE_FETCH=1: explicit local-development network access.
184
+ Never enable this on the public hosted MCP.
93
185
 
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.
186
+ For a self-hosted AIFP-1 gateway, set `AIFINPAY_GATEWAY_ORIGINS` to exact HTTPS
187
+ origins. `AIFINPAY_GATEWAY_PATH_MODE=direct` makes the process use each request's
188
+ full pathname as its AIFP-1 resource identity; `gateway` uses the merchant slug.
189
+ The mode is process-wide, does not broaden the trusted-origin allowlist, and
190
+ does not enable the retired signing tools in the production RC.
99
191
 
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.
192
+ Dev quoting does not bypass wallet signatures, issuer verification or
193
+ receipt metering. See the bundled skill for backend prerequisites. The low-level SDK has an Amoy
194
+ executor; this MCP candidate does not expose Amoy paid receipt purchases.
103
195
 
104
196
  ## License
105
197
 
@@ -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,12 +1,23 @@
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. */
9
16
  timeoutMs?: number;
17
+ /** Owner opt-in; never accepted from tool arguments. */
18
+ paymentsEnabled?: boolean;
19
+ dailyAmountUsd?: number;
20
+ maxGasPol?: string;
10
21
  /** Hard cap on a single payment to prevent runaway agents. */
11
22
  maxAmountUsd?: number;
12
23
  /** Gateway origins this agent may settle against.
@@ -22,6 +33,8 @@ export interface McpConfig {
22
33
  * unrecognised host means paying whoever answered. Naming a host here is a
23
34
  * statement that you know who that is. */
24
35
  gatewayOrigins?: string[];
36
+ /** AIFP-1 resource identity: merchant slug or full direct request path. */
37
+ gatewayPathMode?: "gateway" | "direct";
25
38
  /** Hosts whose DNS pre-check is skipped, exact names only.
26
39
  *
27
40
  * safe-fetch resolves a hostname and refuses if any answer is a private
@@ -37,4 +50,6 @@ export interface McpConfig {
37
50
  logFn?: (level: "info" | "warn" | "error", msg: string) => void;
38
51
  }
39
52
  export declare function loadConfigFromEnv(): McpConfig;
53
+ /** Signing is available only after the owner configures all spending boundaries. */
54
+ export declare function validatePaymentConfig(config: McpConfig): bigint;
40
55
  //# sourceMappingURL=config.d.ts.map
@@ -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,wDAAwD;IACxD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,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,CAyB7C;AA0DD,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CA+B/D"}