Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
- name: vet + build
run: |
go vet .
go test .
go build -o lexe-mcp .

- name: smoke
Expand Down Expand Up @@ -46,3 +47,11 @@ jobs:
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
http://127.0.0.1:18010/mcp | tee /tmp/tools.json
grep -q 'lexe.create_invoice' /tmp/tools.json
grep -q 'lexe.pay' /tmp/tools.json
grep -q 'lexe.analyze' /tmp/tools.json

curl -sf -m 3 -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"lexe.pay","arguments":{"invoice":"lnbc1ci"}}}' \
http://127.0.0.1:18010/mcp | tee /tmp/pay.json
grep -q 'missing Lexe identity' /tmp/pay.json
grep -q '"isError":true' /tmp/pay.json
7 changes: 6 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ from the optional L402 hop and still proxy upstream.

If the request has no Bearer and the server has no
`lexeClientCredentials` in config, `lexe.create_invoice` /
`lexe.check_payment` fail with `missing Lexe identity`.
`lexe.pay` / `lexe.check_payment` fail with `missing Lexe identity`.

### Goose

Expand Down Expand Up @@ -190,10 +190,14 @@ These work **without** L402 so a client can discover and pay:
| Name | Args | Result |
|---|---|---|
| `lexe.create_invoice` | `description?`, `amount_sats?` | BOLT12 offer string |
| `lexe.pay` | `invoice` or `offer`, `amount_sats?`, `note?`, `proof_note?` | `{settled, index, payment}` and, for settled offer pays, `proof` (`lnp1…`) + `proof_url` (`https://lnproof.space/lnp1…`) |
| `lexe.analyze` | `invoice` or `offer` | decoded amount/kind, no send |
| `lexe.check_payment` | `index` | `{settled, payment}` |
| `lexe.my_account` | `index` | `{email, bearer_token}` after settlement |
| `lexe.node_health` | — | sidecar health |

`lexe.pay` is the “pay this invoice `<lnbc…|lno1…>`” tool. It analyzes first, refuses on-chain, then calls sidecar `pay_invoice` / `pay_offer` / `pay_lnurl`. Amountless strings need `amount_sats`. Optional `payMaxSats` / `LEXE_PAY_MAX_SATS` (0 = no cap). After a settled BOLT12 offer, it calls `create_payer_proof` and returns the lnproof.space link; a proof failure does not undo the payment (`proof_error` is set). Sidecar failures and missing identity are `isError: true`.

Modern `tools/call` must send `Mcp-Name` matching `params.name`. Results are
`resultType: "complete"` + `content[]` + `structuredContent`.

Expand Down Expand Up @@ -224,6 +228,7 @@ Modern `tools/call` must send `Mcp-Name` matching `params.name`. Results are
| `upstreamMcpToken` / `UPSTREAM_MCP_TOKEN` | — | L402 hop — MCP HTTP Bearer that `:8014` expects (not `ak_`) |
| `publicUrl` / `PUBLIC_URL` | — | advertised in 402 `payment_request_url` |
| `amountSats` / `L402_AMOUNT_SATS` | `1` | L402 hop — min sats on the BOLT12 offer |
| `payMaxSats` / `LEXE_PAY_MAX_SATS` | `0` | outbound `lexe.pay` cap; `0` = no cap |
| `stateDir` | `~/.lexe-mcp` | |

Neither credential fatals at boot. Lightning-only public host: leave both
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and exposes Lightning as MCP tools:
| Tool | What it does |
|---|---|
| `lexe.create_invoice` | Create a reusable BOLT12 offer (min amount in sats) |
| `lexe.pay` | Pay a BOLT11 invoice (`lnbc…`) or BOLT12 offer (`lno1…`) from the caller's node. Offer pays also return a payer proof + `https://lnproof.space/lnp1…` |
| `lexe.analyze` | Decode an invoice/offer without sending |
| `lexe.check_payment` | Look up a payment by index; report settled or not |
| `lexe.my_account` | After settlement, return the minted access token |
| `lexe.node_health` | Sidecar / node health |
Expand Down
1 change: 1 addition & 0 deletions config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"upstreamMcpToken": "PASTE_AGENTICMAIL_MCP_HTTP_TOKEN",
"publicUrl": "https://emailagent.hedwig.sh",
"amountSats": 1,
"payMaxSats": 0,
"invoiceDescription": "AgenticMail MCP access - one-time payment, unlimited use",
"offerTtlSecs": 3600,
"stateDir": "~/.lexe-mcp"
Expand Down
47 changes: 47 additions & 0 deletions docs/brainstorms/2026-09-11-outbound-analyze-and-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## Clarified Problem Statement

**Goal:** Let an MCP client say “pay this invoice `<lnbc…|lno1…>`” and have lexe-mcp send from the caller’s Lexe node.

**User lock-in (2026-09-12):** L402 is out of scope. The UX is a single tool call with the invoice or offer string — not a 402 auto-payer.

**Inferences:**
- WHO: caller sending `Authorization: Bearer <lexeClientCredentials>` (same as `lexe.create_invoice`).
- WHAT: `lexe.pay` analyzes via sidecar, refuses on-chain, then `pay_invoice` / `pay_offer` / `pay_lnurl`. Settled BOLT12 offer pays also mint a payer proof (`lnp1…`) and `https://lnproof.space/<proof>`. `lexe.analyze` inspects without sending.
- WHY: server was receive-only.
- SCOPE: Lightning only. Dual-era MCP, stdlib-only. Sidecar failures are `isError: true`.
- SUCCESS: `tools/list` has `lexe.pay` + `lexe.analyze`; `lexe.pay` without Bearer → missing identity; amountless needs `amount_sats`; `check_payment` works on the outbound index.

**Constraints:**
- Stdlib only; dual-era; per-request Bearer; no secrets in git.
- `pay_*` blocks until Lightning terminal — client timeout ≥ ~60–180s.
- Amountless payables require `amount_sats`. Encoded amount must match if both are set.

**Non-goals:**
- L402 receive hop changes, auto-pay of other MCP 402s, on-chain, Cash App, LNURL-withdraw.

**Success criteria:**
- `lexe.pay { invoice }` / `{ offer }` sends and returns `{settled, index, payment}`. Settled offer pays return `proof` + `proof_url` on lnproof.space.
- `lexe.analyze` returns amount/kind without sending.
- On-chain kind refused. CI lists the new tools and greps missing-identity on unpaid `lexe.pay`.

## Approaches Considered

### Approach A: Explicit `lexe.analyze` + `lexe.pay` (chosen)
- Sketch: Analyze then specific sidecar pay endpoint. Optional `payMaxSats` (0 = no cap).
- Affected files: `server.go`, `server_test.go`, `AGENTS.md`, `README.md`, `.github/workflows/ci.yml`, `config.example.json`.
- Tradeoffs: Agent stays in control. No silent 402 spender.
- Effort: S

### Approach B: Transparent L402 payer
- Deferred. User does not want this now.

### Approach C: Quote-then-confirm
- Deferred. Extra round-trip vs “just pay this invoice”.

## Recommendation

**Approach A**, implemented. Primary tool is `lexe.pay`. L402 left as-is.

## Open questions

- Whether to cap `payMaxSats` on the public host (default 0 / unlimited).
Loading
Loading