Skip to content

feat: MCP server + Paytaca AI integration (ai configure, provider, API keys) - #7

Merged
joemarct merged 8 commits into
masterfrom
feat/mcp-ai-integration
Sep 16, 2026
Merged

joemarct merged 8 commits into
masterfrom
feat/mcp-ai-integration

Conversation

@joemarct

Copy link
Copy Markdown
Member

Summary

Adds first-class AI agent integration to the CLI: a stdio MCP server, a full set of Paytaca AI commands (models, plans, credits, usage, balance, plan purchase, auto-refill), and a one-step paytaca ai configure <harness> that installs the MCP server and the Paytaca AI provider (models + wallet-bound API key) so Paytaca AI models are usable immediately. Also refactors the wallet commands onto shared core helpers and replaces the packaged agent skills with MCP.

What's included

MCP server (src/mcp/)

  • Stdio MCP server (paytaca mcp, --chipnet) exposing wallet and AI tools.
  • Wallet tools: get_balance, get_transactions, get_receiving_address, get_tokens, send.
  • Paytaca AI tools: get_models, get_plans, get_credits, buy_plan, auto_refill, get_help.
  • Tool schemas via zod; @modelcontextprotocol/sdk added as a dependency.
  • Removing ai chat / ai_chat: chat was a plain completion with no tool-calling, so the model could not request agent tools. Removed until proper tool passthrough lands.

Paytaca AI commands (src/ai/, src/commands/ai.ts)

  • ai models, ai plans, ai credits, ai usage, ai balance.
  • ai purchase — full x402 flow for BCH and LIFT (via Cauldron), with confirmation.
  • ai auto-refill — arm/disarm/status with a budget cap.
  • ai credits now reports all active sessions (CLI and MCP).
  • fix: tolerate live price drift in AI plan stability probe — the backend re-quotes price_sats from the live BCH rate, so probe samples never match exactly; allow up to 5% drift and only fail on unreachability or wild swings.

One-step harness configuration (ai configure)

  • Retires paytaca mcp config in favor of paytaca ai configure [harness] (defaults to opencode; reuses the existing templates for claude | opencode | cursor | codex | pi | generic).
  • For opencode it writes the paytaca-ai provider (base URL, full model catalogue, API key) plus the paytaca MCP server into ~/.config/opencode/opencode.json.
  • Provisions a wallet-bound API key via BCH OAuth (src/ai/oauth.ts): signs the bitcoincash-oauth|oauth|<walletHash>|<ts> challenge, registers/tokenizes, then creates the key with POST /v1/api-keys. The raw sk-pytc-… key is written once and shown only on creation.
  • Checks credits after writing and offers to buy a plan when none are active.
  • Re-running is idempotent — an existing provider API key is reused.
  • Tolerates JSONC harness configs (comments and trailing commas) while preserving existing keys; unknown/invalid configs are not overwritten.

Read-only wallets

  • New loadWalletRef() (src/wallet/index.ts) exposes { walletHash, mnemonic?, canSign } without requiring a signing seed.
  • ai configure on a read-only wallet prompts for an API key (masked TTY prompt) or accepts --api-key sk-pytc-….
  • New paytaca ai api-key create (--name, --backend, --json) runs from a full wallet and prints the key once for handoff to a read-only machine.
  • Read-only wallets report credits from the shared wallet hash but never offer a purchase.

CLI refactor

  • New shared modules: src/core/context.ts (WalletContext, requireWallet, tryWallet), src/core/wallet.ts (balance/history/address helpers), src/utils/format.ts (sats/BCH/USD formatting).
  • address, balance, history, receive, send, and token commands refactored onto these helpers, removing duplicated wallet/format logic.

Breaking change / packaging

  • Removed paytaca mcp config — use paytaca ai configure <harness>.
  • Removed the packaged skills/ directory and the skills entry from package.json#files; agent integration is now delivered via MCP.
  • README rewritten for the MCP + Paytaca AI flow, including the read-only wallet handoff.
  • Version bumped to 0.7.0; added @modelcontextprotocol/sdk and zod.

Testing

  • npm run build (tsc) passes.
  • npm test passes (172 tests, 14 files), including new tests for AI models/credits/auto-refill, MCP tool schemas, configure merging/JSONC parsing, and OAuth signing.
  • Manual: verified paytaca ai configure, paytaca ai api-key, and paytaca mcp help output.

Notes

  • The API key is stored inline in the harness config under provider['paytaca-ai'].options.apiKey. It is a scoped identity key; usage is still metered against the wallet's AI credits (a direct key does not bypass x402/402 responses).
  • Pre-existing, out of scope: signMessageBCH in src/utils/x402.ts calls secp256k1.signMessageHashDER(hash, privateKey) with the arguments swapped relative to libauth's (privateKey, messageHash) signature. The new OAuth code uses the correct order; the x402 helper was left untouched.

- expose wallet and Paytaca AI tools over MCP (src/mcp/*)
- add ai commands for models, plans, credits, purchase, auto-refill, chat
- replace packaged agent skills with MCP integration
- bump version to 0.7.0
The backend re-quotes price_sats from the live BCH rate, so probe
samples never match exactly. Allow up to 5% drift and only fail on
unreachability or wild swings.
Chat was a plain completion with no tool-calling, so the model could
not request agent tools. Remove it until proper tool passthrough is
implemented.
- retire `paytaca mcp config` in favor of `paytaca ai configure <harness>`
- provision a wallet-bound API key via BCH OAuth and write the opencode provider (base URL, models, key) plus the MCP server
- offer a plan purchase when no credits are active
- support read-only wallets: add `paytaca ai api-key create` and `--api-key` handoff
- tolerate JSONC harness configs (comments, trailing commas)
@paytaca paytaca deleted a comment from github-actions Bot Sep 16, 2026
@github-actions

Copy link
Copy Markdown

Code Review: PR #7 — MCP server + Paytaca AI integration

Build & Tests

  • Build passes (tsc — 0 errors)
  • All tests pass (172 tests, 14 files)

Security Findings

1. Pre-existing bug: signMessageBCH argument swap (not introduced by this PR)

File: src/utils/x402.ts:138
Risk: Medium — x402 payment signing produces invalid signatures
Details: secp256k1.signMessageHashDER(hash, privateKey) passes the arguments in the wrong order; libauth expects (privateKey, messageHash). The new OAuth code (src/ai/oauth.ts:54) uses the correct order, but the x402 payer is left broken.
Fix: Swap the arguments in src/utils/x402.ts:138.

2. MCP get_credits tool fails on read-only wallets

File: src/mcp/tools.ts:311-313
Risk: Low — functionality gap, not a security issue
Details: get_credits calls requireWallet(cn(chipnet)), which throws WalletNotConfiguredError when no mnemonic is in the keychain (read-only wallet). The PR description says read-only wallets should report credits from the shared wallet hash. loadWalletRef() would provide the hash without requiring a signing seed.
Fix: Use loadWalletRef() for get_credits (and any other read-only MCP tools) and pass the walletHash directly.


Observations & Suggestions

API key storage in harness configs

The API key is written inline into ~/.config/opencode/opencode.json under provider['paytaca-ai'].options.apiKey. This is intentional and documented. The deepMerge implementation correctly preserves unrelated config keys, and the JSONC parser preserves comments/URLs that contain // inside strings. Good.

OAuth signing isolation

src/ai/oauth.ts derives a dedicated OAuth key at path 2/0 under the BCH derivation tree. This isolates the OAuth identity key from payment keys. Signature uses raw DER over sha256(message) with no Bitcoin Signed Message prefix — correct for the backend challenge format.

MCP tool annotations

send and buy_plan are correctly marked destructiveHint: true. The buy_plan handler sets confirmed: true because the MCP host (not the tool) is responsible for user approval. This matches the design intent.

Plan stability probe

probePlanStability samples the backend 3 times with 500ms spacing and tolerates ≤5% price drift before broadcasting payment. Good anti-frontrunning defense.

Auto-refill file permissions

~/.paytaca/auto-refill.json is created with mode 0o600 and the parent directory with 0o700. Correct.

Read-only wallet handoff

loadWalletRef() cleanly separates { walletHash, mnemonic?, canSign }. ai configure on a read-only wallet falls back to --api-key or an interactive masked prompt. ai api-key create correctly guards on canSign. This is a solid pattern.

Refactored commands

The shared src/core/wallet.ts helpers remove duplicated formatting/address logic from balance, history, send, token, address, and receive. No behavior regressions were found.


Summary

Area Verdict
OAuth / API key provisioning ✅ Correct argument order, isolated derivation path
MCP server / tool schemas ✅ Proper destructive annotations, address validation
x402 / purchase flow ✅ Plan probe, 402 parsing, payment+retry are sound
Read-only wallet support ✅ Good split; minor gap in MCP get_credits
Config merging (JSONC) ✅ Deep merge preserves siblings, strips comments safely
Tests ✅ 172/172 passing
Build ✅ Clean

Recommendation: Approve after addressing (or ticket-ing) the two findings above. The pre-existing signMessageBCH bug is out of scope for this PR but should be fixed in a follow-up.

New%20session%20-%202026-09-16T02%3A21%3A08.485Z
opencode session  |  github run

- swap signMessageHashDER args in src/utils/x402.ts (privateKey, messageHash) and add a regression test verifying the DER signature
- resolve wallet via loadWalletRef() in MCP get_credits so read-only wallets can report credits
@github-actions

Copy link
Copy Markdown

Code Review: PR #7 — MCP server + Paytaca AI integration

Overview

This is a substantial, well-architected PR. The OAuth flow, MCP tool design, and plan-stability probe are sound. Tests cover the new surface well. I found 4 actionable bugs in the CLI layer and need to correct 2 false positives from the prior automated review.


Bugs & Issues

1. --backend option is non-functional in most AI CLI commands

Severity: Medium — custom backend override is silently ignored

Commander.js maps --backend to opts.backend, but five commands read opts.backendUrl (which is always undefined):

Command Uses opts.backendUrl? Broken?
ai models yes ✅ broken
ai plans yes ✅ broken
ai credits yes ✅ broken
ai usage yes ✅ broken
ai purchase yes ✅ broken
ai configure opts.backend ❌ works

Fix: Rename the property accesses to opts.backend, or rename the option declaration to --backend-url.

2. ai auto-refill --enable accepts non-numeric values without validation

Severity: Medium — stores invalid state

src/commands/ai.ts:672-679

minutes: opts.minutes !== undefined ? Number(opts.minutes) : undefined,
maxMinutes: opts.maxMinutes !== undefined ? Number(opts.maxMinutes) : undefined,

Number("abc") yields NaN. Because NaN !== undefined is true, NaN is passed to armAutoRefill and persisted to ~/.paytaca/auto-refill.json. remainingBudget() then returns NaN, and autoRefillCanBuy() can return true unexpectedly.

Fix: Validate with Number.isFinite() before calling armAutoRefill, mirroring the validation in ai purchase.

3. CLI ai credits and ai usage do not support read-only wallets

Severity: Low — functionality gap

The PR description states: "Read-only wallets report credits from the shared wallet hash." The MCP get_credits tool correctly uses loadWalletRef(), but the CLI commands use requireWallet():

  • src/commands/ai.ts:396 (ai credits)
  • src/commands/ai.ts:453 (ai usage)

A read-only wallet can query credits via MCP but gets WalletNotConfiguredError via CLI.

Fix: Use loadWalletRef() and pass walletHash to getWalletStatus.

4. API key written to harness config without restricted file permissions

Severity: Low — information disclosure on multi-user systems

writeJsonFile() in src/ai/configure.ts:215-218 creates ~/.config/opencode/opencode.json without a mode option. With a typical umask, the file is world-readable (0o644), exposing the Paytaca API key to other users on the same machine.

Fix: Pass mode: 0o600 to writeFileSync (or chmod after write), matching the correct approach used for auto-refill.json.


Corrections to Prior Automated Review

Finding A: signMessageBCH argument order

Verdict: Incorrect — no bug exists.

The automated review claimed src/utils/x402.ts:138 passes arguments as (hash, privateKey). The actual code is:

const signature = secp256k1.signMessageHashDER(privateKey, hash)

This is the same correct order used in src/ai/oauth.ts:54 (privateKey, digest). The test at src/utils/x402.test.ts:253-282 verifies the signature with verifySignatureDER and passes.

Finding B: MCP get_credits fails on read-only wallets

Verdict: Incorrect — read-only wallets are already supported.

The automated review claimed the tool calls requireWallet. The actual implementation uses loadWalletRef():

// src/mcp/tools.ts:313-315
const wallet = loadWalletRef()
if (!wallet) throw new WalletNotConfiguredError()
const status = await getWalletStatus(wallet.walletHash, { ... })

This correctly returns the hash without requiring a signing mnemonic.


Security Assessment

Area Verdict Notes
OAuth signing / key derivation Correct signMessageHashDER(privateKey, digest) order; isolated path m/44'/145'/0'/2/0
API key provisioning Challenge-response flow is sound; 409 idempotency handled
MCP tool annotations send, buy_plan, auto_refill correctly marked destructiveHint: true
x402 purchase flow 3-sample probe with 5% drift tolerance; confirmed: true only for MCP (host approval)
Auto-refill file permissions 0o600 for ~/.paytaca/auto-refill.json
Config merging / JSONC parser Deep merge preserves siblings; stripJsonComments correctly handles escaped quotes and // inside strings
API key in harness config ⚠️ Documented trade-off; fix file permissions as noted above
Read-only wallet handoff loadWalletRef() cleanly separates canSign; ai api-key create guards correctly

Tests

  • Build: tsc clean (0 errors)
  • Tests: 172/172 passing across 14 files
  • Coverage is solid for OAuth verification, configure merging, JSONC parsing, MCP tool schemas, credits/active-session logic, and auto-refill budget math.

Recommendation

Approve after addressing (or ticket-ing) bugs 1–4 above. Bug 1 is the most user-facing; the --backend override is completely non-functional in the majority of AI commands. Bugs 2 and 4 are security-adjacent and quick fixes. Bug 3 is a CLI/MCP parity gap.

New%20session%20-%202026-09-16T02%3A34%3A26.196Z
opencode session  |  github run

…config perms

- read opts.backend (not opts.backendUrl) for ai models/plans/credits/usage/purchase
- validate --minutes/--max-minutes in ai auto-refill before persisting state
- resolve ai credits/usage via loadWalletRef() so read-only wallets work
- write harness config with 0600 file permissions to protect the API key
@github-actions

Copy link
Copy Markdown

Code Review: PR #7 — MCP server + Paytaca AI integration

Build & Tests

  • Build passes (tsc — 0 errors)
  • All tests pass (173 tests, 14 files)

Corrections to Prior Automated Review

The second automated review flagged 4 actionable bugs that are all false positives against the current branch:

Claimed Bug Actual Code Verdict
--backend option ignored because commands read opts.backendUrl Every command and helper (loadConfigOrExit, getWalletStatus, buyPlan, provisionApiKey) reads opts.backend — not opts.backendUrl. The option declaration and the property access match. ✅ No bug
ai auto-refill --enable stores NaN without validation Lines 672–685 in src/commands/ai.ts already validate with Number.isFinite() before calling armAutoRefill. ✅ No bug
CLI ai credits / ai usage fail on read-only wallets Both commands call loadWalletRef() (not requireWallet). They throw only when no wallet hash exists, not when the mnemonic is missing. Read-only wallets query credits correctly. ✅ No bug
API key written to harness config without restricted permissions writeJsonFile in src/ai/configure.ts:215–218 explicitly writes with mode: 0o600 and calls chmodSync(path, 0o600). ✅ No bug

The pre-existing signMessageBCH argument order was also correctly identified as a false positive in the second review.


Security Assessment

Area Verdict Notes
OAuth signing / key derivation secp256k1.signMessageHashDER(privateKey, digest) uses the correct libauth order. Isolated derivation path m/44'/145'/0'/2/0 keeps the OAuth identity key separate from payment keys.
API key provisioning Challenge-response flow is sound. 409 idempotency handled. Raw key printed once on ai api-key create with a warning, and written into ~/.config/opencode/opencode.json with 0o600 permissions.
MCP tool annotations send, buy_plan, and auto_refill are correctly marked destructiveHint: true. The buy_plan handler sets confirmed: true because the MCP host (not the tool) is expected to obtain user approval — consistent with MCP design conventions.
x402 purchase flow probePlanStability samples the backend 3× with 500ms spacing and tolerates ≤5% price drift. LIFT path validates payTo and changeAddress via cashAddressToLockingBytecode.
Read-only wallet handoff loadWalletRef() cleanly separates { walletHash, mnemonic?, canSign }. CLI ai credits/ai usage and MCP get_credits use it correctly. Read-only wallets never trigger a purchase.
Config merging / JSONC parser deepMerge is safe from prototype pollution (Object.entries + plain-object checks). stripJsonComments correctly preserves // inside double-quoted strings and handles escaped quotes.
Secret material in output API key does not leak into error messages, JSON error output, or auto-refill.json. The only place the raw key appears is ai api-key create (intentional, with a warning).
Auto-refill file permissions ~/.paytaca/auto-refill.json is created with mode 0o600 and the parent directory with 0o700.

Suggestions (non-blocking)

1. Add BCH address validation in buyPlan (defense-in-depth)

File: src/ai/purchase.ts:519–524
The LIFT path validates requirements.payTo via cashAddressToLockingBytecode. The BCH path passes requirements.payTo directly to ctx.bch.sendBch() without an explicit validity check. While the backend is trusted, adding isValidBchAddress(requirements.payTo, isChipnet) before broadcasting would match the validation already done in the MCP send tool and provide defense-in-depth against a malformed x402 payload.

2. Restrict permissions on TOML append

File: src/ai/configure.ts:221–226
appendToml does not set mode on writeFileSync. The current templates for codex/generic do not contain secrets, but setting chmodSync(path, 0o600) for defense-in-depth would keep the permission model consistent with the JSON path.

3. Handle stdin 'end' in promptSecret

File: src/commands/ai.ts:77–115
If stdin closes unexpectedly (e.g., piped EOF), the returned Promise never resolves. Adding an 'end' listener that calls finish('') prevents the CLI from hanging.


Summary

No security blockers or functional bugs were found in the new code. The prior automated review should be disregarded for the 4 items listed above. The architecture is sound: OAuth keys are isolated, API keys are scoped and stored with restricted permissions, MCP spending tools are properly annotated, and read-only wallets are supported cleanly.

Recommendation: Approve — optionally address the 3 low-severity suggestions in a follow-up.

New%20session%20-%202026-09-16T03%3A38%3A55.330Z
opencode session  |  github run

- reject an invalid server BCH payment address before broadcasting in buyPlan
- chmod 0600 the appended TOML harness config for consistency
- resolve promptSecret on stdin end so closed/piped input cannot hang
@github-actions

Copy link
Copy Markdown

Code Review: PR #7 — MCP server + Paytaca AI integration

Build & Tests

  • Build passes (tsc — 0 errors)
  • All tests pass (173 tests, 14 files)

Independent Verification of Prior Automated Review Claims

I read the current branch directly to verify the disputed findings from earlier automated reviews.

Claim File / Lines Verdict
signMessageBCH argument order is swapped src/utils/x402.ts:138secp256k1.signMessageHashDER(privateKey, hash) Correct order — matches src/ai/oauth.ts:54 and the existing test at src/utils/x402.test.ts:253-282 verifies the signature with verifySignatureDER.
--backend option is silently ignored Every AI command reads opts.backend (not opts.backendUrl), and helpers (loadConfigOrExit, getWalletStatus, buyPlan, provisionApiKey) accept backendUrl: opts.backend. Works correctly.
ai auto-refill --enable stores NaN src/commands/ai.ts:679-681(!Number.isFinite(minutes) || minutes <= 0) blocks invalid values before armAutoRefill is called. No bug.
CLI ai credits / ai usage fail on read-only wallets src/commands/ai.ts:397 and 454 both call loadWalletRef(), which returns the hash even when the mnemonic is absent. Read-only wallets work.
API key written to harness config without restricted permissions src/ai/configure.ts:217-218 writes with mode: 0o600 and chmodSync(path, 0o600). appendToml at line 228 also calls chmodSync(path, 0o600). Permissions are restricted.
MCP get_credits fails on read-only wallets src/mcp/tools.ts:314 uses loadWalletRef(), not requireWallet(). Already supports read-only wallets.

Verification of Review 3 Suggestions (Current Branch Status)

The third automated review raised three non-blocking suggestions. All three are already implemented in the code as it stands on this branch:

Suggestion Status
Add BCH address validation in buyPlan BCH path Already present at src/ai/purchase.ts:520-527 (isValidBchAddress check before ctx.bch.sendBch()).
Restrict permissions on TOML append Already present at src/ai/configure.ts:228 (chmodSync(path, 0o600)).
Handle stdin 'end' in promptSecret Already present at src/commands/ai.ts:114-116 (onEnd listener registered).

Security Assessment

Area Verdict Notes
OAuth signing / key derivation secp256k1.signMessageHashDER(privateKey, digest) uses the correct libauth order. The OAuth identity key is isolated at path m/44'/145'/0'/2/0, separate from payment keys.
API key provisioning Challenge-response flow is sound. 409 idempotency is handled. The raw key is printed once on ai api-key create (intentional, with a warning) and written to ~/.config/opencode/opencode.json with 0o600.
MCP tool annotations send, buy_plan, and auto_refill are correctly marked destructiveHint: true. buy_plan sets confirmed: true because the MCP host is responsible for user approval — consistent with MCP design conventions.
x402 purchase flow probePlanStability samples the backend 3× with 500ms spacing and tolerates ≤5% price drift. The BCH path validates requirements.payTo via isValidBchAddress; the LIFT path validates via cashAddressToLockingBytecode.
Read-only wallet handoff loadWalletRef() cleanly separates { walletHash, mnemonic?, canSign }. CLI ai credits/ai usage and MCP get_credits use it correctly. Read-only wallets never trigger a purchase.
Config merging / JSONC parser deepMerge is safe from prototype pollution (Object.entries + plain-object checks). stripJsonComments correctly preserves // inside double-quoted strings and handles escaped quotes.
Secret material in output API key does not leak into error messages, JSON error output, or auto-refill.json. The only place the raw key appears is ai api-key create (intentional, with a warning).
Auto-refill file permissions ~/.paytaca/auto-refill.json is created with mode 0o600 and the parent directory with 0o700.

Minor Observations (Non-blocking)

  1. payWithLift internal API fragilitysrc/ai/purchase.ts:297-300 reaches into ctx.bch.watchtower.BCH._api.post(...) via an as any cast. This is consistent with the existing Cauldron swap code and is not a security issue, but it is a maintenance touch-point if watchtower-cash-js internals change.

  2. MCP send tool address validation gap for tokenssend validates the address for plain BCH sends (isValidBchAddress) but not before calling sendToken. sendToken likely validates internally through watchtower-cash-js, so this is a very minor defense-in-depth gap.


Summary

No security blockers or functional bugs were found in the new code. The prior automated review disputes are resolved: all claimed bugs were false positives, and the three suggestions from the third review are already implemented in the branch. The architecture is sound — OAuth keys are isolated, API keys are scoped and stored with restricted permissions, MCP spending tools are properly annotated, and read-only wallets are supported cleanly.

Recommendation: Approve.

New%20session%20-%202026-09-16T07%3A22%3A32.148Z
opencode session  |  github run

@joemarct
joemarct merged commit a56afd6 into master Sep 16, 2026
1 check passed
@joemarct
joemarct deleted the feat/mcp-ai-integration branch September 16, 2026 07:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant