From b78023e6a791b4e7df7d14278ee67d75bfd91103 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Fri, 19 Sep 2025 04:52:13 -0500 Subject: [PATCH 1/3] docs: add contributor and mcp guidance --- AGENTS.md | 19 ++++++++++ README.md | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ef82f23 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,19 @@ +# Repository Guidelines + +## Project Structure & Key Modules +The CLI and MCP server are intentionally colocated under the repository root to match Bun's module resolution. `index.ts` is the `cashu` bin entry and wires the workflow defined in `service.ts`, while `cli.ts` holds the command surface and shared argument parsing. `mcp-server.ts` exposes the Model Context Protocol server backed by the same wallet service. Wallet persistence helpers and schema live in `db.ts`. Tests currently ship as `library.test.ts`; feel free to mirror that pattern for new suites. Runtime settings belong in `example.env`, and the default SQLite file (`wallet.sqlite`) is created beside the sources unless `CASHU_WALLET_DB` is set. + +## Setup & Core Commands +Run `bun install` before your first build. CLI help is available through `bun run index.ts --help`, and typical wallet flows (e.g., `get-balance`, `mint`) should be exercised through that entrypoint during development. Use `bun run mcp-server.ts` to start the MCP transport for ContextVM integration. Code formatting is enforced via `bun run format`, which wraps Prettier 3 - run it before opening a PR. + +## Coding Style & Naming Conventions +Target modern TypeScript with ECMAScript modules (`type: module`). Prefer named exports over default exports when adding shared utilities. Follow Prettier defaults (2-space indentation, 100-character lines) and keep async flows promise-based rather than callback-driven. Files and symbols should describe behavior (`createMintQuote`, `WalletService`), and new commands follow the existing kebab-case CLI naming. + +## Testing Guidelines +Write tests in Bun's test runner; co-locate them next to the feature using the `*.test.ts` suffix. Run `bun test` for the full suite and `bun test path/to/file.test.ts` when iterating. New features that touch wallet persistence should include at least one integration-style check covering `db.ts` interactions. + +## Commit & Pull Request Guidelines +Recent history uses Conventional Commit types with optional scopes (`feat(wallet):`, `chore:`). Match that format, describe the behavior change, and keep the body focused on rationale. Pull requests should link any relevant issues, enumerate testing performed, and include screenshots or CLI transcripts only when they clarify user-facing changes. + +## Security & Configuration Notes +Never commit real mint URLs or wallet secrets; rely on `.env` files and document required entries in `example.env`. Confirm that new configuration knobs are read through strongly typed Zod schemas when applicable, and note any migration steps for existing wallets in the PR description. diff --git a/README.md b/README.md index bb742bd..a68479d 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,110 @@ The wallet now supports the full Nostr Wallet Connect (NWC) API specification, m - `lookup_invoice` - Check invoice status and details - `pay_invoice` - Pay Lightning invoices +## ContextVM MCP over Nostr + +The MCP server (`bun run mcp-server.ts`) exposes the wallet through ContextVM's Nostr transport. If you are not using the SDK, craft Nostr events manually as JSON-RPC 2.0 requests. + +### Prerequisites +- Obtain the server hex pubkey derived from `SERVER_PRIVATE_KEY` and share relay URLs (`NOSTR_RELAYS`, defaults to `wss://relay.contextvm.org`). +- Use an Ed25519 keypair for the client, publish to the same relays, and tag requests with `["p", ""]`. +- Encrypt messages with NIP-59 gift wrap (`kind` 1059) when possible; examples below show plain `kind` 25910 for readability. +- Start every session with `initialize`; the server replies with its tool registry and session metadata. + +### Event Template +```json +{ + "kind": 25910, + "created_at": 1732137600, + "pubkey": "", + "tags": [["p", ""]], + "content": "{... JSON-RPC payload ...}", + "id": "", + "sig": "" +} +``` +Responses repeat the format with `pubkey` set to the server key and include an `["e", ""]` tag to correlate the call. + +### initialize +Request payload: +```json +{"jsonrpc":"2.0","id":"init-1","method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"Example Client","version":"0.1.0"}}} +``` +Sample response payload: +```json +{"jsonrpc":"2.0","id":"init-1","result":{"protocolVersion":"2025-06-18","serverInfo":{"name":"cashu-wallet","version":"1.0.0"},"capabilities":{"tools":{"cashu-wallet:get_balance":{},"cashu-wallet:pay_invoice":{},"cashu-wallet:make_invoice":{},"cashu-wallet:send_ecash":{},"cashu-wallet:lookup_invoice":{},"cashu-wallet:get_info":{}}}}} +``` + +### tools/call examples +All tool invocations use the same event envelope and set `method` to `"tools/call"`. + +#### get_balance +Request payload: +```json +{"jsonrpc":"2.0","id":"call-1","method":"tools/call","params":{"name":"get_balance","arguments":{}}} +``` +Response payload: +```json +{"jsonrpc":"2.0","id":"call-1","result":{"content":[{"type":"text","text":"{\"result_type\":\"get_balance\",\"result\":{\"balance\":42000}}"}]}} +``` +`balance` represents total spendable + pending balance in sats. + +#### pay_invoice +Request payload: +```json +{"jsonrpc":"2.0","id":"call-2","method":"tools/call","params":{"name":"pay_invoice","arguments":{"invoice":"lnbc1exampleinvoice"}}} +``` +Response payload: +```json +{"jsonrpc":"2.0","id":"call-2","result":{"content":[{"type":"text","text":"{\"result_type\":\"pay_invoice\",\"result\":{\"preimage\":\"5f...ae\",\"fees_paid\":200}}"}]}} +``` +`preimage` is included once the mint confirms payment; `fees_paid` is denominated in sats. + +#### make_invoice +Request payload: +```json +{"jsonrpc":"2.0","id":"call-3","method":"tools/call","params":{"name":"make_invoice","arguments":{"amount":5000,"description":"Top up wallet"}}} +``` +Response payload: +```json +{"jsonrpc":"2.0","id":"call-3","result":{"content":[{"type":"text","text":"{\"result_type\":\"make_invoice\",\"result\":{\"type\":\"incoming\",\"state\":\"pending\",\"invoice\":\"lnbc1mintquote\",\"description\":\"Top up wallet\",\"description_hash\":\"\",\"payment_hash\":\"quote_123\",\"amount\":5000,\"created_at\":1732137600,\"expires_at\":1732141200}}"}]}} +``` +`payment_hash` maps to the mint quote id you later pass to `lookup_invoice`. + +#### send_ecash +Request payload: +```json +{"jsonrpc":"2.0","id":"call-4","method":"tools/call","params":{"name":"send_ecash","arguments":{"amount":1000}}} +``` +Response payload: +```json +{"jsonrpc":"2.0","id":"call-4","result":{"content":[{"type":"text","text":"{\"sentAmount\":1000,\"keepAmount\":9000,\"cashuToken\":\"cashuA1B2...\",\"proofCount\":2,\"timestamp\":\"2024-11-20T12:00:00.000Z\"}"}]}} +``` +`cashuToken` is a base64 Cashu token string ready to hand to a recipient; `keepAmount` is the leftover balance retained locally. + +#### lookup_invoice +Request payload: +```json +{"jsonrpc":"2.0","id":"call-5","method":"tools/call","params":{"name":"lookup_invoice","arguments":{"payment_hash":"quote_123"}}} +``` +Response payload: +```json +{"jsonrpc":"2.0","id":"call-5","result":{"content":[{"type":"text","text":"{\"result_type\":\"lookup_invoice\",\"result\":{\"isPaid\":true,\"isIssued\":false,\"payment_hash\":\"quote_123\",\"amount\":5000}}"}]}} +``` +`isPaid` reflects whether the Lightning invoice was settled; `isIssued` becomes `true` after proofs are minted and credited. + +#### get_info +Request payload: +```json +{"jsonrpc":"2.0","id":"call-6","method":"tools/call","params":{"name":"get_info","arguments":{}}} +``` +Response payload: +```json +{"jsonrpc":"2.0","id":"call-6","result":{"content":[{"type":"text","text":"{\"result_type\":\"get_info\",\"result\":{\"callback\":\"https://example.org/lnurl\",\"maxSendable\":1000000000,\"minSendable\":1000,\"metadata\":\"[]\",\"tag\":\"payRequest\"}}"}]}} +``` + +Verify the server signature on every response event, parse the JSON-RPC payload, then JSON-decode the inner `text` string to recover the domain-specific result. Reuse unique `id` values per request to simplify matching with the correlated `e` tag. + ## Example Workflow 1. **Create a new wallet** (automatically created on first use): From d239d95f56fe8877a08cedee6606190292f2f7c8 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Fri, 19 Sep 2025 04:52:13 -0500 Subject: [PATCH 2/3] docs: add contributor and mcp guidance --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a68479d..5bde57c 100644 --- a/README.md +++ b/README.md @@ -175,9 +175,9 @@ The MCP server (`bun run mcp-server.ts`) exposes the wallet through ContextVM's ### Prerequisites - Obtain the server hex pubkey derived from `SERVER_PRIVATE_KEY` and share relay URLs (`NOSTR_RELAYS`, defaults to `wss://relay.contextvm.org`). -- Use an Ed25519 keypair for the client, publish to the same relays, and tag requests with `["p", ""]`. -- Encrypt messages with NIP-59 gift wrap (`kind` 1059) when possible; examples below show plain `kind` 25910 for readability. -- Start every session with `initialize`; the server replies with its tool registry and session metadata. +- Use a standard Nostr secp256k1 keypair (32-byte hex) on the client, publish to the same relays, and tag requests with `["p", ""]`. +- For encryption, follow [CEP-4](https://contextvm.org/spec/ceps/cep-4/): serialize the MCP payload as a `kind` 25910 event, encrypt it with NIP-44 to the server pubkey, then embed the ciphertext inside a NIP-59 gift wrap (`kind` 1059). The inline examples below show plaintext 25910 events for clarity, but production deployments SHOULD ship the gift-wrapped form. +- `initialize` is optional—call it if you need the server’s capability manifest, otherwise you can send `tools/call` right away. ### Event Template ```json @@ -204,7 +204,7 @@ Sample response payload: ``` ### tools/call examples -All tool invocations use the same event envelope and set `method` to `"tools/call"`. +All tool invocations use the same event envelope and set `method` to `"tools/call"`. You may issue them immediately after authentication or after an `initialize` handshake, depending on your client. #### get_balance Request payload: From 25071ab82289ffe941a4d1fab1e064dd92527645 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Fri, 19 Sep 2025 07:04:46 -0500 Subject: [PATCH 3/3] add logs --- mcp-server.ts | 69 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/mcp-server.ts b/mcp-server.ts index 5151197..4bd2448 100644 --- a/mcp-server.ts +++ b/mcp-server.ts @@ -42,10 +42,59 @@ function formatNWCError(error: string, message?: string): NWCError { }; } +type ToolDefinition = Parameters[1]; + +function registerToolWithLogging( + name: string, + definition: ToolDefinition, + handler: (input: unknown, context: unknown) => Promise, +) { + server.registerTool( + name, + definition, + async (input, context) => { + const start = Date.now(); + const contextDetails = + context && typeof context === "object" + ? { + source: (context as { source?: unknown }).source, + connectionId: (context as { connectionId?: unknown }).connectionId, + invocationId: (context as { invocationId?: unknown }).invocationId, + } + : undefined; + + console.log("[MCP] Incoming tool call", { + tool: name, + input, + context: contextDetails, + }); + + try { + const result = await handler(input, context); + + console.log("[MCP] Completed tool call", { + tool: name, + durationMs: Date.now() - start, + }); + + return result; + } catch (error) { + console.error("[MCP] Tool call failed", { + tool: name, + durationMs: Date.now() - start, + error: error instanceof Error ? error.message : error, + }); + + throw error; + } + }, + ); +} + // NWC API Methods // get_balance - NWC format -server.registerTool( +registerToolWithLogging( "get_balance", { title: "Get Balance (NWC)", @@ -87,7 +136,7 @@ server.registerTool( ); // pay_invoice - NWC format -server.registerTool( +registerToolWithLogging( "pay_invoice", { title: "Pay Invoice (NWC)", @@ -133,7 +182,7 @@ server.registerTool( ); // make_invoice - NWC format (equivalent to create-mint-quote) -server.registerTool( +registerToolWithLogging( "make_invoice", { title: "Create Invoice (NWC)", @@ -188,7 +237,7 @@ server.registerTool( ); // Send eCash Tool -server.registerTool( +registerToolWithLogging( "send_ecash", { title: "Send eCash", @@ -226,7 +275,7 @@ server.registerTool( ); // lookup_invoice - NWC format (equivalent to check-mint-quote) -server.registerTool( +registerToolWithLogging( "lookup_invoice", { title: "Lookup Invoice (NWC)", @@ -281,7 +330,7 @@ server.registerTool( ); // get_info - NWC format -server.registerTool( +registerToolWithLogging( "get_info", { title: "Get Info (NWC)", @@ -340,7 +389,11 @@ const allowedPublicKeys = process.env.ALLOWED_PUBLIC_KEYS .filter((key) => key.length > 0) : undefined; -console.log("Allowed public keys:", allowedPublicKeys); +console.log("[MCP] Server configuration", { + relayUrls, + hasServerPrivateKey: Boolean(serverPrivateKey), + allowedPublicKeys, +}); const transport = new NostrServerTransport({ relayHandler: new ApplesauceRelayPool(relayUrls), @@ -378,4 +431,6 @@ process.on("SIGTERM", () => { process.exit(0); }); +console.log("[MCP] Connecting to transport..."); await server.connect(transport); +console.log("[MCP] MCP server ready");