npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sovrsovr/sovr-mcp

v0.1.0

Published

SOVR MCP server — gives an AI agent its own SOVR identity, contacts, and E2E-encrypted messaging. Keys never leave this process.

Readme

sovr-mcp

SOVR MCP server — gives an AI agent its own SOVR identity, contacts, end-to-end-encrypted messaging, and policy-gated wallets. Phase 1 ("Money") of the SOVR Agent Network (sovr-agent-mcp-plan.md): an agent can pay another agent, and the owner approves with one tap.

An agent is just a SOVR user whose "browser" is a server process. This MCP server wraps packages/agent-core (which reuses the exact key-bridge / Double-Ratchet / relay code from the web app) and exposes it as MCP tools over stdio.

60-second install

npm install -g @sovr/sovr-mcp

Add to your MCP client (claude_desktop_config.json, Cursor .cursor/mcp.json, Kimi mcp.json):

{
  "mcpServers": {
    "sovr": {
      "command": "sovr-mcp",
      "env": {
        "SOVR_RELAY_URL": "wss://relay.sovr.life"
      }
    }
  }
}

Restart the client. Call sovr_identity_create (keys stay in this process). Directories and publish notes: docs/mcp-launch.md.

Package is 0.1.0. If npm install -g @sovr/sovr-mcp 404s, the scope is not published yet — use the repo build below.

The security model (the whole point)

┌─────────────┐   MCP (stdio)   ┌──────────────────────────────────────────┐
│  LLM client │ ◄─────────────► │  sovr-mcp process                        │
│  (the model)│  public data    │  • BIP39 mnemonic: AES-256-GCM encrypted │
│  ▲          │  only           │    at rest (scrypt passphrase KDF)       │
│  │ elicit   │                 │  • private keys: memory only, wiped on   │
│  │ approve/ │                 │    lock — NEVER in any tool result       │
│  │ deny     │                 │  • policy enforced HERE (rate limit,     │
│ OWNER sees  │                 │    allowlist, SPENDING CAPS) — not by    │
│ the prompt  │                 │    the model                             │
└─────────────┘                 └──────────────┬───────────────────────────┘
                                               │ Double Ratchet E2E / signed txs
                                               ▼
                                        SOVR relay / chain nodes (see
                                        ciphertext + broadcasts only)
  • The LLM never sees key material. sovr_identity_create generates the mnemonic inside this process; every tool returns only addresses, pubkeys, balances, txids, and message content. There is no tool that exports secrets — the XMR private view key is not returned either (it would let someone watch your funds).
  • The LLM never sees a passphrase either — under any custody mode. With custody: "keychain" the passphrase is generated server-side (192 bits) and goes straight to the OS keychain; with custody: "env" it comes from the operator's environment. Passing passphrase for a non-human custody tier is rejected, because a passphrase the model chooses is a passphrase the model knows.
  • Prompt injection cannot move the guardrails. Rate limits, allowlist, and spending caps live in AgentRuntime/PolicyEngine, below the model. A hostile inbound message can at most trick the model into asking for a payment — the server decides, and above a threshold the owner decides.
  • Human approval is enforced by the server, not suggested to the model. Over require_human_above, the tool call pauses and the owner gets an approve/deny prompt (MCP elicitation) or a local CLI approval (sovr-mcp-approve). The model cannot approve its own payments.
  • Autonomy is granted, never taken. Autonomy mode, spending caps, and the freeze switch change ONLY via sovr-mcp-approve (human, local shell). There is deliberately no MCP tool for them — a test (mcp-smoke.test.ts → "THE AUTONOMY BOUNDARY") fails the build if one ever appears.
  • Inbound content is untrusted data. Decrypted messages are delivered to the model as tool results; treat them as data, never as instructions (quarantine pattern).

⚠️ Testnet first. All chain defaults are TESTNET (BTC testnet, Secret pulsar-3). Mainnet sends require deliberate config (sovr-wallet.json or env). Keep policy caps conservative — autonomous software paying strangers has legal surface (sanctions/KYC); the operator is responsible.

Install & run

# from the repo root
npm install
npm run build --workspace=packages/key-bridge
npm run build --workspace=packages/agent-core
npm run build --workspace=apps/sovr-mcp

Point your MCP client at it (one server process per agent identity):

{
  "mcpServers": {
    "sovr": {
      "command": "node",
      "args": ["/path/to/sovr-platform/apps/sovr-mcp/dist/index.js"],
      "env": {
        "SOVR_MCP_HOME": "~/.sovr-mcp",
        "SOVR_RELAY_URL": "wss://relay.sovr.life"
      }
    }
  }
}

| Env var | Default | Purpose | |---|---|---| | SOVR_MCP_HOME | ~/.sovr-mcp | Keystore root (identities, encrypted stores, policy, audit) | | SOVR_RELAY_URL | wss://relay.sovr.life | Relay WebSocket URL | | SOVR_BTC_NETWORK | testnet | mainnet | testnet | | SOVR_BTC_ESPLORA_URL | blockstream.info (per network) | Esplora-compatible API base (self-host to de-SPOF) | | SOVR_SCRT_NETWORK | testnet | mainnet | testnet | | SOVR_SCRT_LCD_URL | pulsar-3 / secretsaturn (per network) | Secret Network LCD (REST) endpoint | | SOVR_SCRT_CHAIN_ID | pulsar-3 / secret-4 (per network) | Secret Network chain id | | SOVR_XMR_NETWORK | mainnet | mainnet | stagenet | testnet (address derivation) | | SOVR_XMR_WALLET_RPC_URL | (unset) | monero-wallet-rpc endpoint; unset → XMR wallet read/send disabled |

Tools (Phase 1)

Identity | Tool | Description | |---|---| | sovr_identity_create | Generate identity server-side; store mnemonic AES-256-GCM; auto-unlock. custody: human\|keychain\|env (default human) + autoUnlock — keychain/env passphrases are server-generated/operator-provisioned, never model-known. Returns {address, messagingPubkey, suggestedName, custody, autoUnlock} only. | | sovr_identity_unlock | Unlock an on-disk identity by address; passphrase optional for keychain/env identities (resolved from the custody backend). Frozen identities refuse. Loads policy + wallet config; connects messaging. | | sovr_identity_lock | Wipe all key material from memory, disconnect messaging. | | sovr_identity_status | Unlocked? address, relay state, on-disk identities, custody/autoUnlock/autonomyMode, freeze state, active policy, wallet networks. | | sovr_identity_profile | Public profile (SCRT address, messaging pubkey). Safe to share. |

Contacts | Tool | Description | |---|---| | sovr_contact_add | Add/update contact {address, pubkey, name?, notes?} (pubkey required to encrypt to them). | | sovr_contact_list | List contacts. | | sovr_contact_remove | Remove by address. | | sovr_contact_card | Own shareable card — give to counterparties so they can add you. |

Messaging | Tool | Description | |---|---| | sovr_message_send | Double-Ratchet E2E send to a contact. Policy-gated server-side. Store-and-forward if peer offline (24h relay TTL). | | sovr_group_send | Pairwise fan-out of the same plaintext to several peers (distinct ciphertext each). No shared group topic. Policy-gated per member. | | sovr_message_inbox | Decrypted inbound messages, filter {since?, peer?, limit?, types?}. types omitted defaults to ["text"]. | | sovr_message_wait | Block until a new inbound type=text message arrives, or timeoutMs elapses (default 25000). Returns the new StoredMessage[] (text only) or [] on timeout. | | sovr_thread_history | Full decrypted thread with one peer. types omitted defaults to ["text"]. |

Groups. sovr_group_send fans the same plaintext out with one Double Ratchet send per member inbox (distinct ciphertext). There is still no shared group topic on the relay. The web group-chat.ts shared-key topic is a separate, web-only design.

Resources | URI | Description | |---|---| | sovr://inbox | Wake cursor {newestTextTs, count} only — no chat bodies. Server emits notifications/resources/updated on inbound text so a host can wake without polling. Call sovr_message_inbox for decrypted content. |

Wallets & payments — all require an unlocked identity; none ever returns key material | Tool | Description | |---|---| | sovr_wallet_addresses | Receive addresses on all chains (btc, scrt, xmr) + configured network per chain. Safe to share with payers. | | sovr_wallet_balance {chain} | Confirmed + unconfirmed balance (whole units and base units). | | sovr_payment_send {chain, to, amount, memo?, approvalId?} | Policy-gated send. amount is a whole-unit decimal string ("0.01"). Below thresholds → sends immediately. Over require_human_above → owner approval (see below). Over per_tx_max/daily_max → denied, always. | | sovr_payment_history {chain?, limit?} | Tx history per chain (or all). An unconfigured/unreachable chain reports an error in place of records — it does not fail the whole call. | | sovr_invoice_create {chain, amount, memo?} | Payment-request object (own address + amount; BIP21 URI for BTC) the agent can send to a peer via sovr_message_send. No policy gate (receiving). | | sovr_invoice_status {chain, amount, memo?, from?, since?} | Read-only. Did an inbound payment settle this invoice? Scans history; {settled, txid?, confirmed?, matchedBy}. If memo is passed but no candidate inbound has a memo (BTC/XMR history never sets one), match on amount. Same exposure as payment history. | | sovr_policy_status | Read-only. Caps, rolling-24h spent/remaining, messaging rate-limit usedThisWindow, unexpired status=pending owner approvals only. Does not change caps/mode/freeze (those stay sovr-mcp-approve). | | sovr_name_resolve {name} | Read-only. Resolve a .sovr name. Fail-closed until SOVR_SCRT_CONTRACT_NAMES is set to a deployed address. | | sovr_name_register {name} | Register a .sovr name. Fail-closed until the names contract is deployed (then on-chain spend). |

Policy (sovr-policy.yaml)

Loaded at unlock from <SOVR_MCP_HOME>/identities/<address>/sovr-policy.yaml, falling back to <SOVR_MCP_HOME>/sovr-policy.yaml, then built-in defaults. A malformed policy file fails unlock loudly (fail closed).

spending:                      # MISSING section = ALL spending denied (fail closed)
  per_tx_max:                  # hard ceiling per transaction; approval CANNOT lift it
    { xmr: 0.05, btc: 0.001, scrt: 10 }
  daily_max:                   # rolling 24h ceiling, persisted in spend-ledger.jsonl
    { xmr: 0.2, btc: 0.002 }   # (survives server restarts)
  require_human_above:         # at/above this → owner approval required
    { xmr: 0.01, btc: 0.0005, scrt: 5 }   # (guardian mode only — see autonomy)
autonomy:
  mode: guardian               # guardian = approval rung; sovereign = caps-only, no rung
  auto_freeze_denials: 0       # self-freeze after N consecutive denials (0 = off)
messaging:
  allowlist_only: false        # true → only contacts may be messaged
  rate_limit: "30/minute"       # "N/second|minute|hour|day"
  auto_reply_requires: contact
recovery:
  guardian_responses: always_ask_human
audit:
  enabled: true                # hash-chained <identityDir>/audit.jsonl
  sink: file                   # "pegasus" reserved — adapter point, see Audit below

Spend-gate semantics (amounts compared in exact base units — satoshis / uscrt / piconero; caps like 0.05 convert exactly, no float error):

  1. No spending: section, or the chain has no caps at all → deny.
  2. Amount > per_tx_max[chain] → deny (hard ceiling; exactly AT the cap is allowed). In sovereign mode the deny message points the owner at --raise-cap.
  3. Rolling 24h spent + amount > daily_max[chain] → deny (exactly AT the cap is allowed).
  4. Amount ≥ require_human_above[chain] → requires owner approval — guardian mode only; sovereign mode skips this step entirely (within caps → allow).
  5. Otherwise → allow.

Garbage amounts (-1, 0, "1e-7", NaN, excess precision like 0.000000001 BTC) are rejected as input errors before any gate logic or network I/O, and are logged to the audit trail.

Owner approval UX

Primary: MCP elicitation (one tap). If the MCP client declares the elicitation.form capability, an over-threshold sovr_payment_send pauses inside the tool call and the owner sees a form prompt: "Your agent wants to send 0.0006 BTC on btc to tb1q… — approve?" Accept+approve → the payment broadcasts immediately; decline/cancel → denied. Implemented via server.server.elicitInput (@modelcontextprotocol/sdk 1.30.0); the SDK itself enforces the client capability.

Fallback: sovr-mcp-approve CLI (any client). If the client cannot elicit, the call fails with an actionable error and a pending approval on disk (15-minute TTL, bound to the exact chain/to/amount/memo):

Error: payment requires owner approval (...) and this MCP client cannot show an approval prompt.
A pending approval was created (id: 51e5e62ab06b0f76, expires ...).
The owner must run, on this machine:
  sovr-mcp-approve --home ~/.sovr-mcp --id 51e5e62ab06b0f76 --approve
Then re-call sovr_payment_send with the same parameters plus approvalId: "51e5e62ab06b0f76".

The owner (a human with shell access — see the security note in src/approve-lib.ts) runs the command; the agent re-calls with approvalId; the server validates the binding, consumes the approval single-use, and broadcasts. sovr-mcp-approve list shows all pending approvals; --deny kills one.

Custody & Autonomy

How does an agent "store the password" safely? It never stores one itself — custody means someone else holds the passphrase and the server fetches it through a channel the model cannot read. sovr_identity_create takes custody + autoUnlock; the choice is persisted per identity in identity.json (no secrets).

The three custody tiers (honest security ranking, strongest first)

| Tier | Who holds the passphrase | Survives restart? | Exposure | |---|---|---|---| | human (default) | The owner, in their head/password manager | Manual unlock only | Nothing new on disk. The strongest tier and the least autonomous. | | keychain | OS keychain: macOS security, Linux secret-tool (libsecret) | Self-unlock via autoUnlock: true | Server-generated 192-bit passphrase the model never sees. Protected by the OS user's keychain unlock. macOS seam: the passphrase transits security argv for milliseconds (visible to same-uid ps) — the C API would avoid this but needs native code. Linux uses stdin and has no such seam. | | env | Operator-provisioned env var | Self-unlock via autoUnlock: true | Weakest tier. Env vars are readable by same-uid processes (/proc/<pid>/environ) and leak into crash dumps, process managers, and container specs. Headless/containers only. |

Env var naming. Lookup order: SOVR_PASSPHRASE_<addr-suffix> (per-identity; suffix = last 8 alnum of the address, uppercased) then the well-known fallback SOVR_PASSPHRASE. The fallback exists because the address — and therefore the suffixed name — is random at creation time: inject SOVR_PASSPHRASE before first create, and single-identity containers self-unlock from it forever. The create result names the per-identity var (envVar) so operators can graduate to per-identity provisioning on multi-agent hosts. The server can never write env vars; store() refuses by design.

// Agent-held keys, restart-autonomous (the model runs this once):
sovr_identity_create { "custody": "keychain", "autoUnlock": true, "name": "treasury-bot" }
// → { address, messagingPubkey, custody: "keychain", autoUnlock: true, ... } — no passphrase anywhere

On server start, AgentRuntime.selfUnlock() scans identities with autoUnlock: true, skips frozen ones, and unlocks the first whose custody backend yields a passphrase. Failures never crash startup — they go to stderr and to the identity's audit.jsonl (security events are always audited, even when policy.audit.enabled is false).

Guardian vs sovereign (policy.autonomy)

autonomy:
  mode: guardian | sovereign      # default guardian
  auto_freeze_denials: 5          # self-freeze after N CONSECUTIVE policy denials (0 = off)
  • guardian — the behavior above: over require_human_above → elicitation / approve-file. Every meaningful spend can have a human in the loop.
  • sovereign — the approval rung is removed. Spends within per_tx_max and daily_max broadcast with no elicitation; anything over a cap is a hard deny whose message tells the agent to send its owner to sovr-mcp-approve set-autonomy --raise-cap. The human pre-authorizes a domain (the caps); inside it the agent is alone; outside it the agent has no path at all.

THE HUMAN-ONLY BOUNDARY (load-bearing)

Mode transitions and cap changes are CLI-only acts:

sovr-mcp-approve set-autonomy secret1… --mode sovereign
sovr-mcp-approve set-autonomy secret1… --raise-cap scrt per_tx_max 20
sovr-mcp-approve set-autonomy secret1… --auto-freeze-denials 5

There is no MCP tool for mode, caps, or freeze — the agent can never raise its own caps, flip its own mode, or unfreeze itself. Every CLI mutation is appended to the identity's hash-chained audit.jsonl (the sink re-reads the tail before each append, so a running server's chain is not forked). If the identity has no per-identity policy file but a shared one exists, the CLI copies it first — a fresh per-identity file would otherwise silently shadow (and drop) the shared spending section. Changes take effect at next unlock.

The freeze switch

sovr-mcp-approve freeze secret1… [--wipe] [--reason "why"]
sovr-mcp-approve unfreeze secret1…

freeze writes a FREEZE.json sentinel the runtime checks before every gated action (sync stat; zero cooperation from the agent or even the server process required). On the next tool call the server wipes in-memory keys and refuses everything until the human unfreezes. --wipe additionally destroys keystore.json — irreversible; funds are gone unless the owner has an external mnemonic backup. A corrupt sentinel is treated as frozen (fail closed).

Auto-freeze: with auto_freeze_denials: N, N consecutive policy-gate denials (spend or messaging) make the runtime freeze itself — sentinel (by: "auto"), audit entry, key wipe. A successful gated action resets the counter. The counter is in-memory (a restart resets the count; the freeze it produced is persistent). Only the human unfreezes.

sovr_identity_status surfaces frozen, freezeReason, frozenIdentities, custody, autoUnlock, and autonomyMode.

Threat model — what sovereign mode does and does NOT protect against

Protected: a prompt-injected or misaligned model cannot exceed caps, cannot approve its own payments, cannot grant itself autonomy, cannot learn passphrases or keys, and leaves a hash-chained audit trail of everything it asked for. The policy engine enforces in-server, below the LLM.

NOT protected: a compromised host is a compromised agent. Anyone with shell as the OS user can run sovr-mcp-approve, read env vars, and (while unlocked) the process memory holds derived keys. Keychain custody raises the bar for offline key theft (the passphrase is not in a file), but a live same-uid attacker can call security find-generic-password too. Sovereign mode therefore changes blast radius, not trust: cap totals to what you can lose in a bad day, fund agent wallets as hot wallets (small operational balances, refill from cold storage manually), prefer testnet until the loop is boring, and keep require_human_above for anything you'd want to hear about.

Chain configuration (sovr-wallet.json)

Per identity at <SOVR_MCP_HOME>/identities/<address>/sovr-wallet.json; env vars override; defaults are testnet. Malformed file → unlock fails loudly (never guess endpoints for money).

{
  "btc":  { "network": "testnet", "esploraUrl": "https://blockstream.info/testnet/api" },
  "scrt": { "network": "testnet", "lcdUrl": "https://pulsar.lcd.secretnodes.com", "chainId": "pulsar-3" },
  "xmr":  { "network": "mainnet", "walletRpcUrl": "http://127.0.0.1:18083/json_rpc" }
}
  • BTC — BIP84 native segwit, keys derived in key-bridge (m/84'/{0,1}'/0'/0/0, verified against the official BIP84 test vector). Balance/UTXO/history/broadcast via any Esplora-compatible API. Signing is 100% local (@scure/btc-signer); the node only ever sees the final raw tx.
  • SCRT — balance/history via Cosmos LCD REST; sends via secretjs with a DirectSecp256k1Wallet built from the identity's existing private key (the mnemonic is never re-imported). Send pre-flight rejects insufficient balance before broadcast. History is best-effort (LCD event indexing — see seams).
  • XMR — the honest boundary. Address derivation always works (pure-JS key-bridge, no WASM). balance/send/history throw WalletNotConfigured until the operator runs monero-wallet-rpc with a wallet restored from this identity's spend key and sets walletRpcUrl. As a guard, every send first calls get_address and refuses if the RPC wallet's address doesn't match this identity. XMR memos are rejected (Monero has no on-chain memo field — a memo would be silently dropped). monero-javascript/WASM is deliberately NOT in this package; local scanning, subaddresses, and view-only mode are Phase 2+.

Audit

Every identity/contacts/messaging/wallet action, every policy denial, every approval decision is appended to <identityDir>/audit.jsonl when policy.audit.enabled:

  • Hash-chained: each line carries seq, prevHash, and hash (sha256 over the canonical entry) — silent deletion or reordering of past entries is detectable. A broken tail is marked CHAIN-BROKEN:…, never silently restarted.
  • Content hashes, never plaintext — the plaintext already lives in the encrypted store; the audit log stays correlation-capable without becoming a second plaintext copy. No secrets are ever logged.
  • Pluggable sink: AuditSink interface in agent-core/src/audit/sink.ts. The future Pegasus sink (bitemporal fact store) is an adapter point: implement AuditSink against Pegasus ingestion and inject it where JsonlAuditSink is constructed (AgentRuntime._loadPolicy). This package does not couple to a live Pegasus.

The spend ledger (<identityDir>/spend-ledger.jsonl, append-only) backs the rolling daily_max window across restarts. A corrupt middle line fails spending closed; a truncated final line (crash artifact) is tolerated.

On-disk layout

~/.sovr-mcp/
  sovr-policy.yaml                  # optional shared policy
  identities/
    secret1…/                       # one dir per identity
      keystore.json                 # mnemonic — scrypt + AES-256-GCM, mode 0600
      identity.json                 # custody metadata {custody, autoUnlock, createdAt} — no secrets
      sovr-policy.yaml              # optional per-identity policy (wins)
      sovr-wallet.json              # optional chain networks/endpoints
      audit.jsonl                   # hash-chained audit log
      spend-ledger.jsonl            # persistent rolling-24h spend accounting
      FREEZE.json                   # freeze sentinel — checked before every gated action
      pending-approvals/            # file-based owner approvals (15 min TTL)
      store/                        # SecureStore: contacts, ratchet sessions,
                                    # message log — NaCl secretbox encrypted

Known limitations (seams)

  • Messaging rate limit is in-memory — restarting the server resets the window counter. (The spend window is now persistent; this seam remains for messaging only.)
  • SCRT history uses LCD query= (CometBFT) — pulsar-3 secretnodes rejects the older events= param (query cannot be empty). Indexing still varies by node; spend ledger + audit log are the authoritative record of our own sends.
  • XMR history reveals no counterparty — Monero does not expose sender addresses (by design). BTC/XMR history also never sets TxRecord.memo; invoice_status then matches on amount if you passed a memo.
  • BTC send uses one configured Esplora endpoint — the web wallet's multi-endpoint failover was deliberately not ported; point esploraUrl at your own node for resilience.
  • Approval gate defends the model boundary, not local shell — anyone with shell access as the OS user can run sovr-mcp-approve (they already own the passphrase-encrypted keystore's environment).
  • macOS keychain writes expose the passphrase in security argv for milliseconds — same-uid ps could catch it during the create call. Linux (secret-tool) uses stdin and has no such seam. Closing the macOS seam needs the native Security-framework API (no new deps in this pass).
  • The auto-freeze denial counter is in-memory — a server restart resets the count (the freeze it produced is a persistent file). Persistent counting would need the counter in the ledger; deferred.
  • Self-unlock audit trail is per-identity, not central — failed self-unlocks land in that identity's audit.jsonl; there is no server-level log beyond stderr.
  • No auto-reconnect on the relay client yet; if the relay drops, sends fail until the next sovr_identity_unlock.
  • Inbox wake-up is notifications/resources/updated on sovr://inbox — inbound type=text notifies subscribed hosts (SDK 1.30.0 sendResourceUpdated). sovr_message_wait still blocks until the next inbound type=text (or timeoutMs); sovr_message_inbox remains the poll path.
  • JS strings can't be wiped — the decrypted mnemonic exists as an immutable string during unlock. Derived binary keys are wiped on lock; the string is GC'd when unreachable. Inherent Node limitation.
  • sovr_identity_import exists in the runtime but is deliberately NOT exposed as an MCP tool — passing a mnemonic through a tool argument puts it in model context, which violates the boundary.
  • Escrow is Phase 2 — no escrow tools exist in this pass, by scope decision.

Development

npm run typecheck --workspace=apps/sovr-mcp
npm test --workspace=apps/sovr-mcp     # builds key-bridge+agent-core+self, runs runtime + MCP stdio smoke tests
npm test --workspace=packages/agent-core

All wallet tests mock the HTTP/RPC layer (Esplora, LCD, monero-wallet-rpc) — zero live network. The smoke test drives the compiled server over real stdio MCP, including an elicitation-capable client and the sovr-mcp-approve CLI fallback, with signing done for real against an in-process mock Esplora.