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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# RPC provider — one of: infura, alchemy, custom
# If unset, the MCP server falls back to ~/.recon-crypto-mcp/config.json (written by `recon-crypto-mcp-setup`).
# If unset, the MCP server falls back to ~/.vaultpilot-mcp/config.json (written by `vaultpilot-mcp-setup`).
RPC_PROVIDER=infura

# API key for the chosen provider (unused when RPC_PROVIDER=custom)
Expand Down
36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Recon Crypto MCP
# VaultPilot MCP

[![npm version](https://img.shields.io/npm/v/recon-crypto-mcp.svg)](https://www.npmjs.com/package/recon-crypto-mcp)
[![license](https://img.shields.io/npm/l/recon-crypto-mcp.svg)](./LICENSE)
[![node](https://img.shields.io/node/v/recon-crypto-mcp.svg)](package.json)
[![recon-crypto-mcp MCP server](https://glama.ai/mcp/servers/szhygulin/recon-crypto-mcp/badges/score.svg)](https://glama.ai/mcp/servers/szhygulin/recon-crypto-mcp)
[![npm version](https://img.shields.io/npm/v/vaultpilot-mcp.svg)](https://www.npmjs.com/package/vaultpilot-mcp)
[![license](https://img.shields.io/npm/l/vaultpilot-mcp.svg)](./LICENSE)
[![node](https://img.shields.io/node/v/vaultpilot-mcp.svg)](package.json)
[![vaultpilot-mcp MCP server](https://glama.ai/mcp/servers/szhygulin/vaultpilot-mcp/badges/score.svg)](https://glama.ai/mcp/servers/szhygulin/vaultpilot-mcp)

**Self-custodial crypto portfolio and DeFi, managed by AI agents — signed on your Ledger hardware wallet.**

Recon Crypto MCP is a Model Context Protocol server that lets AI agents — **Claude Code, Claude Desktop, Cursor**, and any MCP-compatible client — read your on-chain positions across **Ethereum, Arbitrum, Polygon, Base**, and **TRON** and prepare EVM transactions that you sign on your **Ledger device via WalletConnect**. Your private keys never leave the hardware wallet, and every transaction is previewed in human-readable form before you approve it on the device.
VaultPilot MCP is a Model Context Protocol server that lets AI agents — **Claude Code, Claude Desktop, Cursor**, and any MCP-compatible client — read your on-chain positions across **Ethereum, Arbitrum, Polygon, Base**, and **TRON** and prepare EVM transactions that you sign on your **Ledger device via WalletConnect**. Your private keys never leave the hardware wallet, and every transaction is previewed in human-readable form before you approve it on the device.

Supported protocols: **Aave V3, Compound V3 (Comet), Morpho Blue, Uniswap V3 LP, Lido (stETH/wstETH), EigenLayer**, plus **LiFi** for swap/bridge aggregation and **1inch** for optional intra-chain quote comparison.

Expand Down Expand Up @@ -69,7 +69,7 @@ Read-only (no Ledger pairing required):

Meta:

- `request_capability` — agent-facing escape hatch: files a GitHub issue on this repo when the user asks for something recon-crypto-mcp can't do (new protocol, new chain, missing tool). Default mode returns a pre-filled issue URL (zero spam risk — user must click to submit). Operators can set `RECON_FEEDBACK_ENDPOINT` to a proxy that posts directly. Rate-limited: 30s between calls, 3/hour, 10/day, 7-day dedupe on identical summaries.
- `request_capability` — agent-facing escape hatch: files a GitHub issue on this repo when the user asks for something vaultpilot-mcp can't do (new protocol, new chain, missing tool). Default mode returns a pre-filled issue URL (zero spam risk — user must click to submit). Operators can set `VAULTPILOT_FEEDBACK_ENDPOINT` to a proxy that posts directly. Rate-limited: 30s between calls, 3/hour, 10/day, 7-day dedupe on identical summaries.

Execution (Ledger-signed via WalletConnect):

Expand All @@ -95,22 +95,22 @@ Execution (Ledger-signed via WalletConnect):
### From npm (recommended)

```bash
npm install -g recon-crypto-mcp
recon-crypto-mcp-setup
npm install -g vaultpilot-mcp
vaultpilot-mcp-setup
```

### From source

```bash
git clone https://github.com/szhygulin/recon-crypto-mcp.git
cd recon-crypto-mcp
git clone https://github.com/szhygulin/vaultpilot-mcp.git
cd vaultpilot-mcp
npm install
npm run build
```

## Setup

Run the interactive setup to pick an RPC provider, validate the key, optionally pair Ledger Live, and write `~/.recon-crypto-mcp/config.json`:
Run the interactive setup to pick an RPC provider, validate the key, optionally pair Ledger Live, and write `~/.vaultpilot-mcp/config.json`:

```bash
npm run setup
Expand All @@ -125,20 +125,20 @@ Add to `claude_desktop_config.json`:
```json
{
"mcpServers": {
"recon-crypto-mcp": {
"command": "recon-crypto-mcp"
"vaultpilot-mcp": {
"command": "vaultpilot-mcp"
}
}
}
```

(If you installed from source rather than via `npm i -g`, swap `"command": "recon-crypto-mcp"` for `"command": "node"` and `"args": ["/absolute/path/to/recon-crypto-mcp/dist/index.js"]`.)
(If you installed from source rather than via `npm i -g`, swap `"command": "vaultpilot-mcp"` for `"command": "node"` and `"args": ["/absolute/path/to/vaultpilot-mcp/dist/index.js"]`.)

The setup script prints a ready-to-paste snippet.

## Environment variables

All are optional if the matching field is in `~/.recon-crypto-mcp/config.json`; env vars take precedence when both are set.
All are optional if the matching field is in `~/.vaultpilot-mcp/config.json`; env vars take precedence when both are set.

- `ETHEREUM_RPC_URL`, `ARBITRUM_RPC_URL`, `POLYGON_RPC_URL`, `BASE_RPC_URL` — custom RPC endpoints
- `RPC_PROVIDER` (`infura` | `alchemy`) + `RPC_API_KEY` — alternative to custom URLs
Expand All @@ -147,8 +147,8 @@ All are optional if the matching field is in `~/.recon-crypto-mcp/config.json`;
- `TRON_API_KEY` — TronGrid API key (sent as `TRON-PRO-API-KEY`). Required in practice to read TRON balances — anonymous TronGrid calls are capped at ~15 req/min, which the portfolio fan-out exceeds. Free to create at [trongrid.io](https://www.trongrid.io).
- `WALLETCONNECT_PROJECT_ID` — required for Ledger Live signing
- `RPC_BATCH=1` — opt into JSON-RPC batching (off by default; many public endpoints mishandle batched POSTs)
- `RECON_ALLOW_INSECURE_RPC=1` — opt out of the https/private-IP check on RPC URLs. Only set this when pointing at a local anvil/hardhat fork; never in production.
- `RECON_FEEDBACK_ENDPOINT` — optional https URL for `request_capability` to POST directly (e.g. a maintainer-operated proxy that creates GitHub issues with a bot token). When unset (the default), `request_capability` returns a pre-filled GitHub issue URL for the user to click through; nothing is transmitted automatically. **Operator responsibility:** the recon-crypto-mcp client does not sign or authenticate POST requests. If you set this endpoint, the proxy MUST enforce its own auth (IP allowlist, Cloudflare Access, HMAC header validation, etc.) — otherwise any caller who learns the URL can submit to it. The on-process rate limiter (3/hour, 10/day) is a courtesy, not a security control.
- `VAULTPILOT_ALLOW_INSECURE_RPC=1` — opt out of the https/private-IP check on RPC URLs. Only set this when pointing at a local anvil/hardhat fork; never in production. (Old name `RECON_ALLOW_INSECURE_RPC` is still honored for one release.)
- `VAULTPILOT_FEEDBACK_ENDPOINT` — optional https URL for `request_capability` to POST directly (e.g. a maintainer-operated proxy that creates GitHub issues with a bot token). When unset (the default), `request_capability` returns a pre-filled GitHub issue URL for the user to click through; nothing is transmitted automatically. **Operator responsibility:** the vaultpilot-mcp client does not sign or authenticate POST requests. If you set this endpoint, the proxy MUST enforce its own auth (IP allowlist, Cloudflare Access, HMAC header validation, etc.) — otherwise any caller who learns the URL can submit to it. The on-process rate limiter (3/hour, 10/day) is a courtesy, not a security control. (Old name `RECON_FEEDBACK_ENDPOINT` is still honored for one release.)

## Development

Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "recon-crypto-mcp",
"name": "vaultpilot-mcp",
"version": "0.3.1",
"mcpName": "io.github.szhygulin/recon-crypto-mcp",
"mcpName": "io.github.szhygulin/vaultpilot-mcp",
"description": "MCP server for AI agents (Claude Code, Claude Desktop, Cursor) to manage a self-custodial crypto portfolio through a Ledger hardware wallet. Reads on-chain wallet balances, ENS, token prices, and DeFi positions across Ethereum/Arbitrum/Polygon/Base (Aave V3, Compound V3, Morpho Blue, Uniswap V3 LP, Lido stETH, EigenLayer), surfaces liquidation/health-factor alerts and protocol risk scores, then prepares unsigned EVM transactions (supply, borrow, repay, withdraw, stake, unstake, native/ERC-20 send, and LiFi-routed swaps and cross-chain bridges) that the user signs on their Ledger device via WalletConnect — private keys never leave the hardware wallet.",
"type": "module",
"main": "dist/index.js",
"bin": {
"recon-crypto-mcp": "dist/index.js",
"recon-crypto-mcp-setup": "dist/setup.js"
"vaultpilot-mcp": "dist/index.js",
"vaultpilot-mcp-setup": "dist/setup.js"
},
"files": [
"dist",
Expand Down Expand Up @@ -105,13 +105,13 @@
"multisig",
"timelock"
],
"homepage": "https://github.com/szhygulin/recon-crypto-mcp#readme",
"homepage": "https://github.com/szhygulin/vaultpilot-mcp#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/szhygulin/recon-crypto-mcp.git"
"url": "git+https://github.com/szhygulin/vaultpilot-mcp.git"
},
"bugs": {
"url": "https://github.com/szhygulin/recon-crypto-mcp/issues"
"url": "https://github.com/szhygulin/vaultpilot-mcp/issues"
},
"author": "Viacheslav Zhygulin",
"license": "MIT",
Expand Down
10 changes: 5 additions & 5 deletions server.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.szhygulin/recon-crypto-mcp",
"title": "Recon Crypto MCP",
"name": "io.github.szhygulin/vaultpilot-mcp",
"title": "VaultPilot MCP",
"description": "Self-custodial crypto portfolio: read EVM DeFi, sign on Ledger via WalletConnect.",
"version": "0.3.1",
"websiteUrl": "https://github.com/szhygulin/recon-crypto-mcp",
"websiteUrl": "https://github.com/szhygulin/vaultpilot-mcp",
"repository": {
"url": "https://github.com/szhygulin/recon-crypto-mcp",
"url": "https://github.com/szhygulin/vaultpilot-mcp",
"source": "github"
},
"packages": [
{
"registryType": "npm",
"registryBaseUrl": "https://registry.npmjs.org",
"identifier": "recon-crypto-mcp",
"identifier": "vaultpilot-mcp",
"version": "0.3.1",
"transport": { "type": "stdio" },
"environmentVariables": [
Expand Down
24 changes: 15 additions & 9 deletions src/config/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,31 +48,37 @@ export class RpcConfigError extends Error {
* got mis-pasted (e.g. a neighbour's dev box) and we'd rather fail loud
* than exfiltrate wallet addresses to an unexpected host.
* Callers who intentionally want to hit a local forked node (anvil, hardhat,
* etc.) can opt out with RECON_ALLOW_INSECURE_RPC=1.
* etc.) can opt out with VAULTPILOT_ALLOW_INSECURE_RPC=1 (legacy alias
* RECON_ALLOW_INSECURE_RPC is still honored for one release).
*/
export function validateRpcUrl(chain: SupportedChain, url: string): void {
if (process.env.RECON_ALLOW_INSECURE_RPC === "1") return;
if (
process.env.VAULTPILOT_ALLOW_INSECURE_RPC === "1" ||
process.env.RECON_ALLOW_INSECURE_RPC === "1"
) {
return;
}
let parsed: URL;
try {
parsed = new URL(url);
} catch {
throw new RpcConfigError(
`RPC URL for ${chain} is not a valid URL: ${url}. Fix it via \`recon-crypto-mcp-setup\` or the relevant env var.`
`RPC URL for ${chain} is not a valid URL: ${url}. Fix it via \`vaultpilot-mcp-setup\` or the relevant env var.`
);
}
if (parsed.protocol !== "https:") {
throw new RpcConfigError(
`RPC URL for ${chain} must use https (got ${parsed.protocol}//). ` +
`Plaintext RPCs leak wallet addresses to anyone on the network path. ` +
`Set RECON_ALLOW_INSECURE_RPC=1 only if you're pointing at a local anvil/hardhat fork.`
`Set VAULTPILOT_ALLOW_INSECURE_RPC=1 only if you're pointing at a local anvil/hardhat fork.`
);
}
const host = parsed.hostname.toLowerCase();
if (isPrivateOrLoopbackHost(host)) {
throw new RpcConfigError(
`RPC URL for ${chain} points at a private/loopback host (${host}). ` +
`This is almost always a mis-pasted config. ` +
`Set RECON_ALLOW_INSECURE_RPC=1 if you intend to hit a local fork.`
`Set VAULTPILOT_ALLOW_INSECURE_RPC=1 if you intend to hit a local fork.`
);
}
}
Expand Down Expand Up @@ -129,7 +135,7 @@ function isPrivateOrLoopbackIPv4(a: number, b: number): boolean {

/**
* Resolve the RPC URL for a given chain based on env vars (highest priority)
* then the user's ~/.recon-crypto-mcp/config.json.
* then the user's ~/.vaultpilot-mcp/config.json.
*/
export function resolveRpcUrl(chain: SupportedChain, userConfig: UserConfig | null): string {
const url = resolveRpcUrlRaw(chain, userConfig);
Expand Down Expand Up @@ -159,13 +165,13 @@ function resolveRpcUrlRaw(chain: SupportedChain, userConfig: UserConfig | null):
const url = customUrls?.[chain];
if (url) return url;
throw new RpcConfigError(
`No custom RPC URL configured for chain "${chain}". Re-run \`recon-crypto-mcp-setup\`.`
`No custom RPC URL configured for chain "${chain}". Re-run \`vaultpilot-mcp-setup\`.`
);
}
if (provider === "infura" || provider === "alchemy") {
if (!apiKey) {
throw new RpcConfigError(
`Missing API key for RPC provider "${provider}". Re-run \`recon-crypto-mcp-setup\`.`
`Missing API key for RPC provider "${provider}". Re-run \`vaultpilot-mcp-setup\`.`
);
}
return PROVIDER_URL_TEMPLATES[provider][chain](apiKey);
Expand All @@ -174,6 +180,6 @@ function resolveRpcUrlRaw(chain: SupportedChain, userConfig: UserConfig | null):

throw new RpcConfigError(
`No RPC provider configured for chain "${chain}". ` +
`Run \`recon-crypto-mcp-setup\` to configure Infura, Alchemy, or a custom endpoint.`
`Run \`vaultpilot-mcp-setup\` to configure Infura, Alchemy, or a custom endpoint.`
);
}
29 changes: 23 additions & 6 deletions src/config/user-config.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,47 @@
import { readFileSync, writeFileSync, mkdirSync, existsSync, lstatSync } from "node:fs";
import { readFileSync, writeFileSync, mkdirSync, existsSync, lstatSync, cpSync } from "node:fs";
import { homedir } from "node:os";
import { join, dirname } from "node:path";
import type { UserConfig } from "../types/index.js";

const CONFIG_DIR = join(homedir(), ".recon-crypto-mcp");
const CONFIG_DIR = join(homedir(), ".vaultpilot-mcp");
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
// Pre-rename path. We still read from here if the new dir doesn't exist, and
// copy the legacy dir on first write so existing users keep their WC pairing
// state (walletconnect.db) across the rename.
const LEGACY_CONFIG_DIR = join(homedir(), ".recon-crypto-mcp");
const LEGACY_CONFIG_PATH = join(LEGACY_CONFIG_DIR, "config.json");

/** Read the user config file; returns null if it doesn't exist. Throws on malformed JSON. */
export function readUserConfig(): UserConfig | null {
if (!existsSync(CONFIG_PATH)) return null;
const raw = readFileSync(CONFIG_PATH, "utf8");
const path = existsSync(CONFIG_PATH)
? CONFIG_PATH
: existsSync(LEGACY_CONFIG_PATH)
? LEGACY_CONFIG_PATH
: null;
if (!path) return null;
const raw = readFileSync(path, "utf8");
try {
return JSON.parse(raw) as UserConfig;
} catch (err) {
throw new Error(
`~/.recon-crypto-mcp/config.json is malformed: ${(err as Error).message}. Delete it or re-run \`recon-crypto-mcp-setup\`.`
`~/.vaultpilot-mcp/config.json is malformed: ${(err as Error).message}. Delete it or re-run \`vaultpilot-mcp-setup\`.`
);
}
}

export function writeUserConfig(config: UserConfig): void {
// Migrate the legacy `.recon-crypto-mcp` dir to the new `.vaultpilot-mcp`
// location on first write after upgrade. We `cp -r` rather than rename so the
// user can roll back if something goes sideways. The legacy dir stays put;
// a future release can drop it.
if (!existsSync(CONFIG_DIR) && existsSync(LEGACY_CONFIG_DIR)) {
cpSync(LEGACY_CONFIG_DIR, CONFIG_DIR, { recursive: true, preserveTimestamps: true });
}
if (!existsSync(CONFIG_DIR)) {
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
}
// Refuse to follow symlinks or hardlinks when writing the config. A local
// attacker with write access to ~/.recon-crypto-mcp (or with a race-window before
// attacker with write access to ~/.vaultpilot-mcp (or with a race-window before
// first-run setup creates the dir) could pre-place config.json as a symlink
// to another file (~/.ssh/authorized_keys, ~/.bashrc, etc.) so the next
// writeFileSync clobbers it. lstatSync on the path (not following the link)
Expand Down
2 changes: 1 addition & 1 deletion src/data/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export async function verifyChainId(chain: SupportedChain): Promise<void> {
throw new Error(
`RPC for ${chain} returned chainId ${actual}, expected ${expected}. ` +
`The configured endpoint does NOT point at ${chain} — refusing to proceed. ` +
`Fix via \`recon-crypto-mcp-setup\` or the relevant env var.`
`Fix via \`vaultpilot-mcp-setup\` or the relevant env var.`
);
}
verifiedChains.add(chain);
Expand Down
Loading
Loading