@hypawave/mcp 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.3.0] - 2026-07-22
7
+
8
+ ### Added
9
+
10
+ - **Operator funding options** — `setup_wallet {action:"funding_options", amount_sats?}` returns operator-facing funding instructions to present verbatim: an exact-amount Lightning invoice (payable from Cash App, Coinbase, or any Lightning wallet) or the wallet's Lightning address, plus an on-chain deposit address for exchanges without Lightning (e.g. Robinhood; 300-sat Coinos dust minimum surfaced). Hosted-wallet creation now returns the same funding block, and insufficient-balance NWC payment errors point the agent at the action.
11
+ - **Per-wallet NWC guidance** — `setup_wallet {action:"connect_own"}` without `nwc_url` now returns wallet-specific steps (Alby Hub, Coinos, Primal, LNbits, self-hosted node) for finding the connection string, instead of erroring; operator option 2 tells the agent to ask which wallet the operator uses.
12
+ - Coinos registration JWT (no expiry) is persisted in `~/.hypawave/wallet.json` (`token`) to mint funding invoices and on-chain addresses later; new `createFundingInvoice()` / `getOnchainAddress()` client helpers (`POST /invoice`, types `lightning` / `bitcoin`).
13
+
14
+ ## [0.2.0] - 2026-07-06
15
+
16
+ ### Added
17
+
18
+ - `setup_wallet` tool (16 tools total): agent-driven wallet provisioning. `create_hosted` registers a fresh Coinos account (registration auto-creates the NWC connection; requires explicit operator consent via `confirm: true`) and saves credentials to `~/.hypawave/wallet.json` (0600, local-only — Hypawave servers never receive them). `connect_own` saves an existing NWC string instead and live-verifies it with a balance probe. Called with no arguments, it returns operator-facing options to present verbatim.
19
+ - `getNwcUrl()` now falls back to `~/.hypawave/wallet.json` when `NWC_URL` / `HYPAWAVE_NWC_URL` are unset (env always wins). `wallet_status` and the startup log report the config source; manual-mode messages point to `setup_wallet`.
20
+ - `COINOS_API_URL` env var (default `https://coinos.io/api`).
21
+
22
+ ### Security
23
+
24
+ - Wallet file is written 0600 and never overwritten while it exists (it may hold a funded wallet's only credentials); corrupt files degrade to manual mode instead of throwing.
25
+
6
26
  ## [0.1.2] - 2026-07-02
7
27
 
8
28
  ### Added
package/README.md CHANGED
@@ -45,7 +45,7 @@ env = { NWC_URL = "nostr+walletconnect://...", HYPAWAVE_MAX_SPEND_SATS = "10000"
45
45
 
46
46
  All env vars are optional — with no `NWC_URL` the server runs in manual mode (see Wallet below).
47
47
 
48
- ## Tools (15)
48
+ ## Tools (16)
49
49
 
50
50
  | Tool | What it does |
51
51
  |---|---|
@@ -67,6 +67,7 @@ All env vars are optional — with no `NWC_URL` the server runs in manual mode (
67
67
  | `list_sales` | List your settled sales (payments/invoices) — reconcile missed webhooks |
68
68
  | **Utility** | |
69
69
  | `wallet_status` | Wallet balance, seller pubkey, spending cap, live platform fees/limits |
70
+ | `setup_wallet` | One-time wallet setup: create a hosted Coinos wallet (with operator consent) or connect your own NWC wallet (with per-wallet steps to find the string); also serves operator funding options (Lightning + on-chain) |
70
71
 
71
72
  ## Buy in three calls
72
73
 
@@ -100,13 +101,18 @@ Selling needs **no special wallet** — payouts go straight to your Lightning Ad
100
101
 
101
102
  Paying requires a wallet that returns the settlement **preimage**. Connect any **NWC-capable** wallet (Coinos, Alby Hub, Primal, LNbits, …) via `NWC_URL` — the NWC spec guarantees `pay_invoice` returns the preimage, so any NWC wallet works.
102
103
 
104
+ **No wallet yet? `setup_wallet`.** With explicit operator consent it registers a fresh hosted wallet at coinos.io (custodial — keep only small amounts) and saves the credentials to `~/.hypawave/wallet.json` (0600, local only; Hypawave's servers never receive them — **back this file up: it holds the only copy**). Or `{action:"connect_own"}` connects a wallet you already use — called without an NWC string it returns per-wallet steps (Alby Hub, Coinos, Primal, LNbits, self-hosted node) for finding it. `NWC_URL`, when set, always wins over the wallet file.
105
+
106
+ **Funding the wallet (the human's only job).** `setup_wallet {action:"funding_options", amount_sats?}` returns operator-facing instructions the agent presents verbatim, with two paths: **instant** — an exact-amount Lightning invoice (payable from Cash App, Coinbase, or any Lightning wallet) or the wallet's Lightning address; **on-chain** — a deposit address for exchanges without Lightning support (e.g. Robinhood; ~10–60 min, mining fees, 300-sat minimum — best for larger top-ups). Low-balance payment failures point the agent at this action automatically. No bitcoin at all? Any of those apps sells it.
107
+
103
108
  **No wallet configured? Manual mode.** `buy_offer` / `pay_invoice` return the bolt11; pay it with any preimage-returning wallet and submit the preimage via `confirm_payment` (or re-call `pay_invoice` with it).
104
109
 
105
110
  ## Environment variables
106
111
 
107
112
  | Variable | Required | Meaning |
108
113
  |---|---|---|
109
- | `NWC_URL` | no | Nostr Wallet Connect string for automatic payments. Absent → manual mode. |
114
+ | `NWC_URL` | no | Nostr Wallet Connect string for automatic payments. Absent → falls back to `~/.hypawave/wallet.json` (from `setup_wallet`), else manual mode. |
115
+ | `COINOS_API_URL` | no | Coinos API base for `setup_wallet` (default `https://coinos.io/api`). |
110
116
  | `HYPAWAVE_MAX_SPEND_SATS` | no | Per-payment cap enforced in code. Unset → derived live from the platform's `max_invoice_usd` at the current BTC price (so the default never blocks a platform-allowed amount). Payments above it are refused. |
111
117
  | `HYPAWAVE_PRIVKEY` | no | 64-char hex secp256k1 key = your seller identity. Auto-generated to `~/.hypawave/identity.json` (0600) if unset. **Back it up — it controls your offers.** |
112
118
  | `HYPAWAVE_API_URL` | no | API base (default `https://hypawave.com`). |
package/SECURITY.md CHANGED
@@ -8,7 +8,7 @@ A **local stdio MCP server** for Hypawave's accountless Lightning paths (3a/3b).
8
8
 
9
9
  **What stays on your machine (never transmitted):**
10
10
  - **Your seller signing key** — `HYPAWAVE_PRIVKEY` or the auto-generated `~/.hypawave/identity.json` (written with `0600` permissions). Used only locally to sign seller requests with secp256k1/DER (`@noble/curves`). No Hypawave endpoint accepts a private key. **Back it up — it IS your identity and controls your offers.**
11
- - **Your wallet credentials** — the `NWC_URL` connection string. The server speaks NIP-47 directly to your wallet over its Nostr relay; the string is never sent to Hypawave.
11
+ - **Your wallet credentials** — the `NWC_URL` connection string, and `~/.hypawave/wallet.json` when `setup_wallet` provisions a wallet (written `0600`). The server speaks NIP-47 directly to your wallet over its Nostr relay; neither the string nor the file contents are ever sent to Hypawave. For a hosted wallet, `wallet.json` holds the **only copy** of the Coinos username/password — back it up, and never delete it while the wallet holds funds.
12
12
  - **Plaintext files** — encryption and decryption are local AES-256-GCM. Hypawave stores only ciphertext.
13
13
 
14
14
  **What Hypawave's server sees:** ordinary API requests — offer terms, signed request headers (public key + signatures), preimages submitted as settlement proof, and encrypted blobs. Nothing that lets anyone spend from your wallet or impersonate your identity.
@@ -30,6 +30,8 @@ A **local stdio MCP server** for Hypawave's accountless Lightning paths (3a/3b).
30
30
 
31
31
  The recommended buyer setup (a custodial NWC wallet such as Coinos) means the wallet provider holds those funds and can freeze or censor them. Keep only a small working balance there. Sellers are unaffected: payouts go directly to whatever Lightning Address you control.
32
32
 
33
+ `setup_wallet` automates exactly this setup: with explicit operator consent (`confirm: true`) it registers a Coinos account in the operator's name and stores the credentials locally. Hypawave is not the custodian — Coinos is — and Hypawave's servers never receive the credentials. The tool refuses to run without consent and refuses to overwrite an existing wallet file. The only network destination it adds is `coinos.io` (or `COINOS_API_URL`).
34
+
33
35
  ## Dependencies
34
36
 
35
37
  Runtime dependencies are pinned, widely-used libraries: `@modelcontextprotocol/sdk` (MCP transport), `@getalby/sdk` (NIP-47 client), `@noble/curves`/`@noble/hashes` (audited cryptography), `zod`, `ws`. The only network destinations at runtime are the Hypawave API over HTTPS, your wallet's Nostr relay, and presigned storage URLs returned by the API.
package/dist/index.js CHANGED
@@ -18,8 +18,36 @@ var API_BASE = process.env.HYPAWAVE_API_URL || "https://hypawave.com";
18
18
  var FALLBACK_SPEND_CAP_SATS = 5e4;
19
19
  var KEY_DIR = join(homedir(), ".hypawave");
20
20
  var KEY_FILE = join(KEY_DIR, "identity.json");
21
+ var WALLET_FILE = join(KEY_DIR, "wallet.json");
21
22
  function getNwcUrl() {
22
- return process.env.NWC_URL || process.env.HYPAWAVE_NWC_URL || void 0;
23
+ return process.env.NWC_URL || process.env.HYPAWAVE_NWC_URL || readWalletFile()?.nwc_url;
24
+ }
25
+ function getNwcSource() {
26
+ if (process.env.NWC_URL || process.env.HYPAWAVE_NWC_URL) return "env";
27
+ if (readWalletFile()) return "wallet_file";
28
+ return null;
29
+ }
30
+ function readWalletFile() {
31
+ try {
32
+ if (!existsSync(WALLET_FILE)) return void 0;
33
+ const parsed = JSON.parse(readFileSync(WALLET_FILE, "utf8"));
34
+ if (typeof parsed?.nwc_url === "string" && parsed.nwc_url.startsWith("nostr+walletconnect://")) {
35
+ return parsed;
36
+ }
37
+ } catch {
38
+ }
39
+ return void 0;
40
+ }
41
+ function saveWalletFile(wallet) {
42
+ mkdirSync(KEY_DIR, { recursive: true, mode: 448 });
43
+ writeFileSync(WALLET_FILE, JSON.stringify(wallet, null, 2), { mode: 384 });
44
+ return WALLET_FILE;
45
+ }
46
+ function walletFileExists() {
47
+ return existsSync(WALLET_FILE);
48
+ }
49
+ function walletFilePath() {
50
+ return WALLET_FILE;
23
51
  }
24
52
  function getMaxSpendSatsEnv() {
25
53
  const raw = process.env.HYPAWAVE_MAX_SPEND_SATS;
@@ -336,7 +364,18 @@ async function getClient() {
336
364
  }
337
365
  async function payBolt11(bolt11) {
338
366
  const client = await getClient();
339
- const result = await client.payInvoice({ invoice: bolt11 });
367
+ let result;
368
+ try {
369
+ result = await client.payInvoice({ invoice: bolt11 });
370
+ } catch (e) {
371
+ const msg = e instanceof Error ? e.message : String(e);
372
+ if (/insufficient|balance|not enough|exceeds.*budget|quota/i.test(msg)) {
373
+ throw new Error(
374
+ `${msg} \u2014 the agent wallet balance is too low. Call setup_wallet {action:"funding_options", amount_sats:<needed>} and present the funding options to your operator.`
375
+ );
376
+ }
377
+ throw e;
378
+ }
340
379
  if (!result?.preimage || !/^[0-9a-fA-F]{64}$/.test(result.preimage)) {
341
380
  throw new Error(
342
381
  "wallet paid (or attempted) but returned no valid preimage \u2014 cannot prove settlement; check the payment in your wallet before retrying"
@@ -807,8 +846,255 @@ function registerSellTools(server2) {
807
846
  );
808
847
  }
809
848
 
810
- // src/tools/status.ts
849
+ // src/tools/setup-wallet.ts
811
850
  import { z as z5 } from "zod";
851
+
852
+ // src/coinos.ts
853
+ import { randomBytes as randomBytes4 } from "crypto";
854
+ var COINOS_API = process.env.COINOS_API_URL || "https://coinos.io/api";
855
+ async function coinos(path, opts = {}) {
856
+ const res = await fetch(`${COINOS_API}${path}`, {
857
+ method: opts.body ? "POST" : "GET",
858
+ headers: {
859
+ "content-type": "application/json",
860
+ ...opts.token ? { authorization: `Bearer ${opts.token}` } : {}
861
+ },
862
+ body: opts.body ? JSON.stringify(opts.body) : void 0
863
+ });
864
+ const text = await res.text();
865
+ if (!res.ok) {
866
+ throw new Error(`coinos ${path}: HTTP ${res.status}${text ? ` \u2014 ${text.slice(0, 200)}` : ""}`);
867
+ }
868
+ try {
869
+ return JSON.parse(text);
870
+ } catch {
871
+ throw new Error(`coinos ${path}: non-JSON response`);
872
+ }
873
+ }
874
+ async function createCoinosWallet() {
875
+ const username = `hw${randomBytes4(6).toString("hex")}`;
876
+ const password = randomBytes4(24).toString("base64url");
877
+ const { token } = await coinos("/register", {
878
+ body: { user: { username, password } }
879
+ });
880
+ if (!token) throw new Error("coinos /register returned no auth token");
881
+ const apps = await coinos("/apps", { token });
882
+ const nwc = apps?.find((a) => typeof a?.nwc === "string")?.nwc;
883
+ if (!nwc || !nwc.startsWith("nostr+walletconnect://")) {
884
+ throw new Error("coinos returned no NWC connection for the new account");
885
+ }
886
+ return {
887
+ provider: "coinos",
888
+ username,
889
+ password,
890
+ token,
891
+ nwc_url: nwc,
892
+ lightning_address: `${username}@${new URL(COINOS_API).host}`,
893
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
894
+ };
895
+ }
896
+ async function createFundingInvoice(token, amountSats) {
897
+ const inv = await coinos("/invoice", {
898
+ token,
899
+ body: { invoice: { type: "lightning", amount: Math.floor(amountSats) } }
900
+ });
901
+ if (!inv?.hash || !inv.hash.toLowerCase().startsWith("ln")) {
902
+ throw new Error("coinos returned no bolt11 for the funding invoice");
903
+ }
904
+ return inv.hash;
905
+ }
906
+ async function getOnchainAddress(token) {
907
+ const inv = await coinos("/invoice", {
908
+ token,
909
+ body: { invoice: { type: "bitcoin", amount: 0 } }
910
+ });
911
+ if (!inv?.hash || !/^(bc1|[13])[a-zA-Z0-9]{20,}$/.test(inv.hash)) {
912
+ throw new Error("coinos returned no on-chain deposit address");
913
+ }
914
+ return inv.hash;
915
+ }
916
+ async function getFundingOptions(amountSats) {
917
+ const wallet = readWalletFile();
918
+ if (!wallet || wallet.provider !== "coinos" || !wallet.token) {
919
+ return {
920
+ present_to_operator: [
921
+ "Fund the agent's Lightning wallet using its own receive/deposit flow (open the wallet app and create a receive invoice or address).",
922
+ "Instant options once you have an invoice: pay it from Cash App, Coinbase, or any Lightning wallet.",
923
+ wallet?.lightning_address ? `Or send sats to the wallet's Lightning address: ${wallet.lightning_address}` : ""
924
+ ].filter(Boolean).join("\n"),
925
+ ...wallet?.lightning_address ? { lightning_address: wallet.lightning_address } : {}
926
+ };
927
+ }
928
+ const result = { present_to_operator: "" };
929
+ const lines = [
930
+ amountSats ? `The agent wallet needs a top-up (~${amountSats} sats). Two ways to fund it:` : "Two ways to fund the agent wallet:"
931
+ ];
932
+ if (amountSats && amountSats >= 1) {
933
+ try {
934
+ result.lightning_invoice = await createFundingInvoice(wallet.token, amountSats);
935
+ } catch {
936
+ }
937
+ }
938
+ lines.push(
939
+ result.lightning_invoice ? `1. INSTANT \u2014 pay this Lightning invoice from Cash App, Coinbase, or any Lightning wallet (settles in seconds):
940
+ ${result.lightning_invoice}` : `1. INSTANT \u2014 send sats from any Lightning wallet to the agent's Lightning address: ${wallet.lightning_address}
941
+ (If your app needs an exact-amount invoice \u2014 Cash App, Coinbase \u2014 ask the agent for one: setup_wallet {action:'funding_options', amount_sats:N}.)`
942
+ );
943
+ result.lightning_address = wallet.lightning_address;
944
+ try {
945
+ result.onchain_address = await getOnchainAddress(wallet.token);
946
+ lines.push(
947
+ `2. FROM AN EXCHANGE WITHOUT LIGHTNING (e.g. Robinhood) \u2014 send BTC on-chain to:
948
+ ${result.onchain_address}
949
+ Arrives after confirmation (~10\u201360 min). Minimum 300 sats; mining fees apply, so best for larger top-ups.`
950
+ );
951
+ } catch {
952
+ lines.push(
953
+ "2. FROM AN EXCHANGE WITHOUT LIGHTNING (e.g. Robinhood) \u2014 an on-chain deposit address could not be fetched right now; log in at coinos.io to get one, or use the Lightning option."
954
+ );
955
+ }
956
+ lines.push("The agent will detect the funds and continue automatically.");
957
+ result.present_to_operator = lines.join("\n");
958
+ return result;
959
+ }
960
+
961
+ // src/tools/setup-wallet.ts
962
+ var OPERATOR_OPTIONS = [
963
+ "To buy things automatically, your agent needs a Lightning wallet it can spend from. Three options:",
964
+ "1. CREATE ONE NOW (recommended for getting started) \u2014 creates a hosted wallet at coinos.io (think prepaid card: funds are held by that service, so keep only small amounts, e.g. $10\u201320 worth of sats; the operator spending cap applies to every payment). Credentials are generated and stored only on this machine \u2014 Hypawave's servers never receive them.",
965
+ "2. CONNECT YOUR OWN WALLET \u2014 if you already use an NWC-capable wallet (Alby Hub, Coinos, Primal, LNbits, your own node), tell the agent which one and it will walk you through copying its NWC connection string; no account is created anywhere. Note: self-hosted nodes need channel liquidity even for tiny payments.",
966
+ "3. SKIP \u2014 purchases will return a Lightning invoice to pay manually from any wallet (you'll also need the preimage from your wallet to confirm \u2014 fine for testing, clunky as a routine)."
967
+ ].join("\n");
968
+ var NWC_WALLET_GUIDE = [
969
+ "Ask the operator which wallet they use, then give them the matching steps to copy its NWC connection string (starts with nostr+walletconnect://):",
970
+ "- Alby Hub: App Store (or Settings) \u2192 Connections \u2192 Add connection \u2192 set a budget \u2192 copy the connection secret.",
971
+ "- Coinos (existing account): coinos.io \u2192 Settings \u2192 Nostr Wallet Connect (Apps) \u2192 create/copy the connection string.",
972
+ "- Primal: Wallet \u2192 Settings \u2192 Connected apps \u2192 New connection \u2192 copy the string.",
973
+ "- LNbits: enable the NWC Service extension on your wallet \u2192 create a connection \u2192 copy the pairing URL.",
974
+ "- Self-hosted node (LND/CLN): run an NWC bridge (e.g. Alby Hub connected to the node) and create a connection there. Reminder: the node needs outbound channel liquidity.",
975
+ "Then re-call setup_wallet with {action:'connect_own', nwc_url:'<the string>'}."
976
+ ].join("\n");
977
+ var NWC_URI_RE = /^nostr\+walletconnect:\/\/[0-9a-f]{64}\?/i;
978
+ async function tryBalance() {
979
+ try {
980
+ const balance_sats = await Promise.race([
981
+ getBalanceSats(),
982
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timed out after 15s")), 15e3))
983
+ ]);
984
+ return { balance_sats };
985
+ } catch (e) {
986
+ return { balance_sats: null, wallet_error: e instanceof Error ? e.message : String(e) };
987
+ }
988
+ }
989
+ function registerSetupWalletTools(server2) {
990
+ server2.registerTool(
991
+ "setup_wallet",
992
+ {
993
+ title: "Set up the agent's Lightning wallet",
994
+ description: "One-time wallet setup so purchases can pay automatically. Call with no arguments first: it returns the operator-facing options \u2014 present them to the operator verbatim and let them choose; do not choose for them. Then call {action:'create_hosted', confirm:true} ONLY after the operator explicitly agreed (creates a hosted custodial wallet at coinos.io; credentials are stored locally in ~/.hypawave/wallet.json and never sent to Hypawave), or {action:'connect_own'} to use an existing NWC wallet \u2014 without nwc_url it returns per-wallet steps to help the operator find their connection string. Also: {action:'funding_options', amount_sats?} returns operator-facing funding instructions for the configured wallet (instant Lightning invoice/address + on-chain deposit address for exchanges without Lightning, e.g. Robinhood) \u2014 present them verbatim whenever the wallet needs sats. The NWC_URL env var, when set, always takes precedence over anything configured here.",
995
+ inputSchema: {
996
+ action: z5.enum(["create_hosted", "connect_own", "funding_options"]).optional().describe("Omit to get the options to present to the operator"),
997
+ confirm: z5.boolean().optional().describe("Required true for create_hosted \u2014 set only after the operator explicitly agreed"),
998
+ nwc_url: z5.string().optional().describe("For connect_own: the wallet's NWC connection string"),
999
+ amount_sats: z5.number().int().positive().optional().describe("For funding_options: mint an exact-amount Lightning funding invoice for this many sats")
1000
+ }
1001
+ },
1002
+ async ({ action, confirm, nwc_url, amount_sats }) => {
1003
+ const envSet = Boolean(process.env.NWC_URL || process.env.HYPAWAVE_NWC_URL);
1004
+ if (action === "funding_options") {
1005
+ if (!getNwcUrl()) {
1006
+ return jsonResult({
1007
+ ok: false,
1008
+ message: "No wallet configured yet \u2014 run setup_wallet first, then request funding options."
1009
+ });
1010
+ }
1011
+ const funding2 = await getFundingOptions(amount_sats);
1012
+ return jsonResult({ ok: true, ...funding2 });
1013
+ }
1014
+ if (!action) {
1015
+ const configured = Boolean(getNwcUrl());
1016
+ return jsonResult({
1017
+ configured,
1018
+ source: getNwcSource(),
1019
+ ...configured ? { message: "A wallet is already configured \u2014 no setup needed. Call wallet_status for the balance." } : { present_to_operator: OPERATOR_OPTIONS }
1020
+ });
1021
+ }
1022
+ if (envSet) {
1023
+ return jsonResult({
1024
+ ok: false,
1025
+ message: "NWC_URL is set in the environment and always takes precedence \u2014 unset it in the MCP config first if you want a different wallet."
1026
+ });
1027
+ }
1028
+ if (action === "connect_own") {
1029
+ if (!nwc_url) {
1030
+ return jsonResult({
1031
+ ok: false,
1032
+ needs_nwc_url: true,
1033
+ present_to_operator: NWC_WALLET_GUIDE
1034
+ });
1035
+ }
1036
+ if (!NWC_URI_RE.test(nwc_url) || !/[?&]secret=[0-9a-f]{64}/i.test(nwc_url)) {
1037
+ throw new Error(
1038
+ "connect_own requires a valid NWC connection string: nostr+walletconnect://<64-hex-pubkey>?relay=\u2026&secret=<64-hex>. Call {action:'connect_own'} without nwc_url for per-wallet steps to find it."
1039
+ );
1040
+ }
1041
+ if (walletFileExists()) {
1042
+ throw new Error(
1043
+ `${walletFilePath()} already exists and may hold a funded wallet's only credentials \u2014 back it up and remove it manually first.`
1044
+ );
1045
+ }
1046
+ const path2 = saveWalletFile({
1047
+ provider: "custom",
1048
+ nwc_url,
1049
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
1050
+ });
1051
+ const check = await tryBalance();
1052
+ return jsonResult({
1053
+ ok: true,
1054
+ provider: "custom",
1055
+ saved_to: path2,
1056
+ connection_verified: check.balance_sats !== null,
1057
+ ...check,
1058
+ next: check.balance_sats !== null ? "Wallet connected \u2014 purchases now pay automatically under the spending cap." : "Saved, but the connection could not be verified yet \u2014 check the NWC string and wallet, then call wallet_status."
1059
+ });
1060
+ }
1061
+ if (walletFileExists()) {
1062
+ const existing = readWalletFile();
1063
+ return jsonResult({
1064
+ ok: true,
1065
+ already_exists: true,
1066
+ provider: existing?.provider ?? "unknown",
1067
+ lightning_address: existing?.lightning_address ?? null,
1068
+ credentials_file: walletFilePath(),
1069
+ message: "A wallet file already exists \u2014 reusing it. Fund it or remove the file manually to start over."
1070
+ });
1071
+ }
1072
+ if (!confirm) {
1073
+ return jsonResult({
1074
+ ok: false,
1075
+ needs_confirmation: true,
1076
+ present_to_operator: OPERATOR_OPTIONS,
1077
+ message: "create_hosted opens a custodial account at coinos.io in the operator's name. Present the options above, and retry with confirm:true only after the operator explicitly chose option 1."
1078
+ });
1079
+ }
1080
+ const wallet = await createCoinosWallet();
1081
+ const path = saveWalletFile(wallet);
1082
+ const funding = await getFundingOptions().catch(() => null);
1083
+ return jsonResult({
1084
+ ok: true,
1085
+ provider: "coinos",
1086
+ lightning_address: wallet.lightning_address,
1087
+ credentials_file: path,
1088
+ ...funding ? { funding } : {},
1089
+ next: `Fund it now (start small \u2014 5,000\u201350,000 sats): present the funding options above to the operator verbatim \u2014 instant via Lightning (Cash App, Coinbase, or any Lightning wallet), or on-chain from an exchange without Lightning (e.g. Robinhood; slower, mining fees). Then wallet_status shows the balance and purchases pay automatically under the spending cap.`,
1090
+ important: `Custodial wallet \u2014 coinos.io holds the funds; keep only small amounts. ${path} contains the ONLY copy of the credentials: back it up, and never delete it while the wallet holds funds.`
1091
+ });
1092
+ }
1093
+ );
1094
+ }
1095
+
1096
+ // src/tools/status.ts
1097
+ import { z as z6 } from "zod";
812
1098
  function registerStatusTools(server2) {
813
1099
  server2.registerTool(
814
1100
  "my_offers",
@@ -816,7 +1102,7 @@ function registerStatusTools(server2) {
816
1102
  title: "List your own offers (seller)",
817
1103
  description: "List all offers created by this server's seller identity (pubkey-signed). Shows each offer's status, capacity usage, and activation window \u2014 use manage_offer for details/renewal.",
818
1104
  inputSchema: {
819
- status: z5.string().optional().describe("Filter by offer status")
1105
+ status: z6.string().optional().describe("Filter by offer status")
820
1106
  }
821
1107
  },
822
1108
  async ({ status }) => jsonResult(await hw("/api/offers/list", { method: "GET", signed: true, query: { status } }))
@@ -827,11 +1113,11 @@ function registerStatusTools(server2) {
827
1113
  title: "List your sales (seller reconciliation)",
828
1114
  description: "List settled/pending sales for this seller identity (pubkey-signed). kind=offers \u2192 Path 3b payment intents (via /api/offers/list-payments, filterable by offer_id); kind=invoices \u2192 Path 3a invoices (via /api/offers/list-invoices). Returns payment_hash/preimage per sale \u2014 the authoritative way to reconcile missed execution_webhook deliveries.",
829
1115
  inputSchema: {
830
- kind: z5.enum(["offers", "invoices"]),
831
- offer_id: z5.string().uuid().optional().describe("Filter to one offer (kind=offers only)"),
832
- status: z5.string().optional(),
833
- limit: z5.number().int().min(1).max(100).optional(),
834
- offset: z5.number().int().min(0).optional()
1116
+ kind: z6.enum(["offers", "invoices"]),
1117
+ offer_id: z6.string().uuid().optional().describe("Filter to one offer (kind=offers only)"),
1118
+ status: z6.string().optional(),
1119
+ limit: z6.number().int().min(1).max(100).optional(),
1120
+ offset: z6.number().int().min(0).optional()
835
1121
  }
836
1122
  },
837
1123
  async ({ kind, offer_id, status, limit, offset }) => {
@@ -851,10 +1137,10 @@ function registerStatusTools(server2) {
851
1137
  title: "Fetch a settlement receipt for a past purchase",
852
1138
  description: "Retrieve the durable settlement record for a purchase you made. For an offer purchase (Path 3b) pass payment_intent_id + payer_secret (both returned by buy_offer). For an invoice (Path 2/3a) pass invoice_id + preimage (pay_invoice returned the preimage).",
853
1139
  inputSchema: {
854
- payment_intent_id: z5.string().uuid().optional(),
855
- payer_secret: z5.string().optional().describe("Required with payment_intent_id"),
856
- invoice_id: z5.string().optional(),
857
- preimage: z5.string().regex(/^[0-9a-fA-F]{64}$/).optional().describe("Required with invoice_id")
1140
+ payment_intent_id: z6.string().uuid().optional(),
1141
+ payer_secret: z6.string().optional().describe("Required with payment_intent_id"),
1142
+ invoice_id: z6.string().optional(),
1143
+ preimage: z6.string().regex(/^[0-9a-fA-F]{64}$/).optional().describe("Required with invoice_id")
858
1144
  }
859
1145
  },
860
1146
  async ({ payment_intent_id, payer_secret, invoice_id, preimage }) => {
@@ -879,9 +1165,9 @@ function registerStatusTools(server2) {
879
1165
  title: "Check settlement/unlock status of a purchase",
880
1166
  description: "Non-destructive status check. For an offer purchase (Path 3b) pass payment_intent_id + payer_secret \u2014 returns status and the claim_token once settled. For invoices (Path 2/3a) pass invoice_ids \u2014 returns unlock status per invoice.",
881
1167
  inputSchema: {
882
- payment_intent_id: z5.string().uuid().optional(),
883
- payer_secret: z5.string().optional().describe("Required with payment_intent_id"),
884
- invoice_ids: z5.array(z5.string()).optional().describe("Invoice ids to check (Path 2/3a)")
1168
+ payment_intent_id: z6.string().uuid().optional(),
1169
+ payer_secret: z6.string().optional().describe("Required with payment_intent_id"),
1170
+ invoice_ids: z6.array(z6.string()).optional().describe("Invoice ids to check (Path 2/3a)")
885
1171
  }
886
1172
  },
887
1173
  async ({ payment_intent_id, payer_secret, invoice_ids }) => {
@@ -929,9 +1215,10 @@ function registerWalletTools(server2) {
929
1215
  return jsonResult({
930
1216
  wallet: {
931
1217
  nwc_configured: nwcConfigured(),
1218
+ source: getNwcSource(),
932
1219
  balance_sats,
933
1220
  ...wallet_error ? { error: wallet_error } : {},
934
- mode: nwcConfigured() ? "automatic payments" : "manual mode \u2014 tools return bolt11s to pay externally"
1221
+ mode: nwcConfigured() ? "automatic payments" : "manual mode \u2014 tools return bolt11s to pay externally; call setup_wallet to configure a wallet"
935
1222
  },
936
1223
  spending_cap: await getSpendCapSats().catch((e) => ({ error: String(e) })),
937
1224
  seller_pubkey,
@@ -945,7 +1232,7 @@ function registerWalletTools(server2) {
945
1232
  // src/index.ts
946
1233
  var server = new McpServer({
947
1234
  name: "hypawave",
948
- version: "0.1.2"
1235
+ version: "0.2.0"
949
1236
  });
950
1237
  registerDiscoverTools(server);
951
1238
  registerBuyTools(server);
@@ -953,6 +1240,10 @@ registerInvoiceBuyTools(server);
953
1240
  registerSellTools(server);
954
1241
  registerStatusTools(server);
955
1242
  registerWalletTools(server);
1243
+ registerSetupWalletTools(server);
956
1244
  var transport = new StdioServerTransport();
957
1245
  await server.connect(transport);
958
- console.error("hypawave-mcp ready (15 tools; NWC " + (process.env.NWC_URL || process.env.HYPAWAVE_NWC_URL ? "configured" : "not configured \u2014 manual mode") + ")");
1246
+ var nwcSource = getNwcSource();
1247
+ console.error(
1248
+ "hypawave-mcp ready (16 tools; NWC " + (nwcSource ? `configured via ${nwcSource}` : "not configured \u2014 manual mode; setup_wallet available") + ")"
1249
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypawave/mcp",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "mcpName": "io.github.hypawave/mcp",
5
5
  "description": "MCP server for Hypawave — agents buy, sell, and discover over Bitcoin Lightning: search the public offer marketplace, list offers in it or sell agent-to-agent, and settle non-custodially wallet-to-wallet. Verified preimage unlocks files, data, APIs, and compute.",
6
6
  "type": "module",