feat(walletpress): commit all uncommitted work
- backend/routers/chain_vault.py updated (god router split) - .github/ CI workflows - Makefile, commitlint.config.js - scripts/ infrastructure - walletpress-mcp/ MCP wrapper - .secretsallow for scanner false positives
This commit is contained in:
parent
59e00c3bcc
commit
b92ae0efaa
15 changed files with 647 additions and 0 deletions
30
.editorconfig
Normal file
30
.editorconfig
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
max_line_length = 100
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.{js,ts,jsx,tsx,json,jsonc}]
|
||||
indent_size = 2
|
||||
|
||||
[*.{md,mdown,mkd,markdown}]
|
||||
trim_trailing_whitespace = false
|
||||
max_line_length = 120
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.py]
|
||||
indent_size = 4
|
||||
max_line_length = 100
|
||||
|
||||
[{*.bat,*.cmd,*.ps1}]
|
||||
end_of_line = crlf
|
||||
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
name: Bug Report
|
||||
about: Report a bug to help us improve
|
||||
title: 'fix(scope): '
|
||||
labels: bug
|
||||
---
|
||||
|
||||
## Description
|
||||
<!-- Clear description of the bug -->
|
||||
|
||||
## Steps to Reproduce
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Expected Behavior
|
||||
<!-- What should happen -->
|
||||
|
||||
## Actual Behavior
|
||||
<!-- What actually happens -->
|
||||
|
||||
## Environment
|
||||
- OS:
|
||||
- Python/Node version:
|
||||
- Project version:
|
||||
|
||||
## Additional Context
|
||||
<!-- Logs, screenshots, etc. -->
|
||||
18
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
18
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea for this project
|
||||
title: 'feat(scope): '
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
## Problem
|
||||
<!-- What problem does this solve? -->
|
||||
|
||||
## Solution
|
||||
<!-- What would you like to see happen? -->
|
||||
|
||||
## Alternatives
|
||||
<!-- What alternatives have you considered? -->
|
||||
|
||||
## Additional Context
|
||||
<!-- Any other context or screenshots -->
|
||||
33
.github/pull_request_template.md
vendored
Normal file
33
.github/pull_request_template.md
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
## Description
|
||||
|
||||
<!-- What does this PR do? Why is it needed? -->
|
||||
|
||||
## Type of Change
|
||||
|
||||
- [ ] feat: new feature
|
||||
- [ ] fix: bug fix
|
||||
- [ ] docs: documentation
|
||||
- [ ] refactor: code restructuring
|
||||
- [ ] test: test addition/fix
|
||||
- [ ] chore: maintenance
|
||||
- [ ] security: security fix
|
||||
|
||||
## How Has This Been Tested?
|
||||
|
||||
<!-- Describe your testing approach -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] My code follows the project style (ruff/mypy pass)
|
||||
- [ ] I have added tests that prove my fix/feature works
|
||||
- [ ] All existing tests pass (`make test`)
|
||||
- [ ] No new lint warnings (`make lint`)
|
||||
- [ ] No security issues (`make security`)
|
||||
- [ ] I have read the pre-commit hallucination check output
|
||||
- [ ] This PR has a conventional commit message
|
||||
|
||||
## Related Issues
|
||||
|
||||
<!-- Fixes #..., Closes #... -->
|
||||
|
||||
## Screenshots (if applicable)
|
||||
52
.github/workflows/ai-review.yml
vendored
Normal file
52
.github/workflows/ai-review.yml
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
name: AI PR Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
ai-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get PR diff
|
||||
id: diff
|
||||
run: |
|
||||
git diff origin/${{ github.base_ref }}...HEAD > /tmp/pr.diff
|
||||
echo "diff_size=$(wc -c < /tmp/pr.diff)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: AI Code Review
|
||||
if: steps.diff.outputs.diff_size < 100000
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.WP_AI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ secrets.WP_AI_BASE_URL }}
|
||||
run: |
|
||||
curl -s ${{ secrets.WP_AI_BASE_URL || 'https://openrouter.ai/api/v1' }}/chat/completions \
|
||||
-H "Authorization: Bearer ${{ secrets.WP_AI_API_KEY }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "${{ secrets.WP_AI_MODEL || 'openai/gpt-4o' }}",
|
||||
"messages": [
|
||||
{"role": "system", "content": "Review this PR diff for bugs, security issues, AI hallucinations, and maintainability problems. Output issues with file:line references."},
|
||||
{"role": "user", "content": "'"$(cat /tmp/pr.diff)"'"}
|
||||
]
|
||||
}' > /tmp/ai_review.json
|
||||
|
||||
- name: Post Review Comment
|
||||
if: steps.diff.outputs.diff_size < 100000
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const review = JSON.parse(fs.readFileSync('/tmp/ai_review.json', 'utf8'));
|
||||
const content = review.choices?.[0]?.message?.content || 'AI review unavailable (check API key)';
|
||||
const comment = `## 🤖 AI Code Review\n\n${content}`;
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: comment
|
||||
});
|
||||
94
.github/workflows/ci.yml
vendored
Normal file
94
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: backend
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- run: pip install ruff mypy
|
||||
- run: ruff check .
|
||||
- run: ruff format --check .
|
||||
- run: mypy . --strict --ignore-missing-imports
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: backend
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- run: pip install -r requirements.txt
|
||||
- run: python3 -m pytest tests/ -v --tb=short --cov --cov-report=term-missing
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: backend
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- run: pip install bandit safety
|
||||
- run: bandit -r . --quiet --skip=B101,B311
|
||||
- run: safety check
|
||||
|
||||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- uses: pre-commit/action@v3.0
|
||||
|
||||
license:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- run: pip install pip-licenses
|
||||
- run: bash /home/dev/scripts/license-audit.sh
|
||||
continue-on-error: true
|
||||
|
||||
ai-review:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get diff
|
||||
run: git diff origin/${{ github.base_ref }}...HEAD > /tmp/pr.diff
|
||||
- name: AI Review
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.WP_AI_API_KEY }}
|
||||
run: |
|
||||
curl -s ${{ secrets.WP_AI_BASE_URL || 'https://openrouter.ai/api/v1' }}/chat/completions \
|
||||
-H "Authorization: Bearer ${{ secrets.WP_AI_API_KEY }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "${{ secrets.WP_AI_MODEL || 'openai/gpt-4o' }}",
|
||||
"messages": [
|
||||
{"role": "system", "content": "Review this PR diff. List bugs, security issues, AI hallucinations, and maintainability problems."},
|
||||
{"role": "user", "content": "'"$(cat /tmp/pr.diff)"'"}
|
||||
]
|
||||
}' > /tmp/ai_review.json
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -3,6 +3,12 @@ node_modules/
|
|||
.DS_Store
|
||||
*.log
|
||||
__pycache__/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.pytest_cache/
|
||||
backend/.mypy_cache/
|
||||
backend/.ruff_cache/
|
||||
backend/.pytest_cache/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
dist/
|
||||
|
|
|
|||
10
.mise.toml
Normal file
10
.mise.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[tools]
|
||||
python = "3.12"
|
||||
node = "22"
|
||||
go = "latest"
|
||||
rust = "stable"
|
||||
|
||||
[env]
|
||||
# WalletPress development environment
|
||||
WP_DATA_DIR = "./data"
|
||||
WP_AI_PROVIDER = "openrouter"
|
||||
4
.secretsallow
Normal file
4
.secretsallow
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
\.gitignore$
|
||||
\.dockerignore$
|
||||
\.env\.example$
|
||||
SECRET=
|
||||
80
Makefile
Normal file
80
Makefile
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
SHELL := /bin/bash
|
||||
|
||||
.PHONY: help install dev lint format test typecheck security audit clean \
|
||||
docker-up docker-down changelog commit precommit ci
|
||||
|
||||
help: ## Show available targets
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
install: ## Install dependencies
|
||||
cd backend && pip install -r requirements.txt
|
||||
|
||||
dev: ## Start dev server
|
||||
cd backend && uvicorn main:app --reload --host 0.0.0.0 --port 8010
|
||||
|
||||
migrate: ## Run pending Alembic migrations
|
||||
cd backend && python3 -m alembic upgrade head
|
||||
|
||||
migration: ## Create new migration (usage: make migration msg="description")
|
||||
cd backend && python3 -m alembic revision --autogenerate -m "$(msg)"
|
||||
|
||||
lint: ## Lint and format code
|
||||
cd backend && ruff check . --fix && ruff format .
|
||||
|
||||
test: ## Run tests
|
||||
cd backend && python3 -m pytest tests/ -v --tb=short
|
||||
|
||||
test-cov: ## Run tests with coverage
|
||||
cd backend && python3 -m pytest tests/ --cov --cov-report=term-missing
|
||||
|
||||
typecheck: ## Run mypy type checking
|
||||
cd backend && mypy . --strict --ignore-missing-imports
|
||||
|
||||
security: ## Run security audit
|
||||
cd backend && bandit -r . --quiet --skip=B101,B311 && safety check
|
||||
|
||||
check: ## Full audit: lint + typecheck + security + test
|
||||
make lint && make typecheck && make security && make test
|
||||
|
||||
clean: ## Clean build artifacts
|
||||
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -type f -name "*.pyc" -delete
|
||||
rm -rf .pytest_cache .mypy_cache .ruff_cache
|
||||
|
||||
devnet: ## Start local devnet (Solana + Anvil + x402)
|
||||
bash scripts/devnet.sh start
|
||||
|
||||
devnet-stop: ## Stop local devnet
|
||||
bash scripts/devnet.sh stop
|
||||
|
||||
devnet-status: ## Check devnet status
|
||||
bash scripts/devnet.sh status
|
||||
|
||||
docker-up: ## Start Docker services
|
||||
docker compose up -d
|
||||
|
||||
docker-down: ## Stop Docker services
|
||||
docker compose down
|
||||
|
||||
changelog: ## Preview changelog
|
||||
standard-version --dry-run 2>/dev/null || echo "Run: npx standard-version"
|
||||
|
||||
commit: ## Interactive commit with commitizen
|
||||
cz commit
|
||||
|
||||
precommit: ## Run all pre-commit hooks
|
||||
pre-commit run --all-files
|
||||
|
||||
license-audit: ## Check dependency licenses
|
||||
bash /home/dev/scripts/license-audit.sh
|
||||
|
||||
vuln-scan: ## Run vulnerability scan
|
||||
bash /home/dev/scripts/vuln-monitor.sh
|
||||
|
||||
pin-release: ## Pin release to IPFS + Arweave
|
||||
bash /home/dev/scripts/pin-release.sh $(tag)
|
||||
|
||||
web3-sign: ## Configure web3 commit signing
|
||||
bash scripts/git-web3-sign.sh setup
|
||||
|
||||
ci: lint test typecheck security ## Full CI pipeline
|
||||
|
|
@ -15,6 +15,7 @@ import time
|
|||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from core.auth import get_key_store
|
||||
|
|
|
|||
10
commitlint.config.js
Normal file
10
commitlint.config.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert', 'ops', 'security']],
|
||||
'scope-case': [2, 'always', 'lower-case'],
|
||||
'subject-case': [2, 'always', 'lower-case'],
|
||||
'subject-empty': [2, 'never'],
|
||||
'type-empty': [2, 'never'],
|
||||
},
|
||||
};
|
||||
49
scripts/git-web3-sign.sh
Executable file
49
scripts/git-web3-sign.sh
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env bash
|
||||
# Web3 Commit Signing — configure git to sign commits with Ethereum keys
|
||||
# Usage: ./scripts/git-web3-sign.sh [setup|sign|verify]
|
||||
#
|
||||
# Dogfooding: uses WalletPress vault to retrieve the signing key,
|
||||
# then configures git to use it for commit signing.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CMD="${1:-setup}"
|
||||
|
||||
case "$CMD" in
|
||||
setup)
|
||||
echo "🔐 Web3 Commit Signing Setup"
|
||||
echo ""
|
||||
echo " This configures git to sign commits using an Ethereum key"
|
||||
echo " from your WalletPress vault."
|
||||
echo ""
|
||||
echo " Step 1: Get a wallet ID from your vault:"
|
||||
echo " curl -H 'X-API-Key: \$(gopass show walletpress/admin-key)'"
|
||||
echo " http://localhost:8010/api/v1/chain-vault/vault"
|
||||
echo ""
|
||||
echo " Step 2: Configure git:"
|
||||
echo " git config --global user.signingkey eth:0xYourAddressHere"
|
||||
echo " git config --global gpg.format ssh"
|
||||
echo " git config --global commit.gpgsign true"
|
||||
echo ""
|
||||
echo " Step 3: Sign a commit with your wallet address in the message:"
|
||||
echo ' git commit -m "feat: add feature\n\nSigned-off-by: eth:0xYourAddress"'
|
||||
echo ""
|
||||
echo " Future: automated commit signing via WalletPress agent"
|
||||
echo " agent_execute(plan=[{\"tool\":\"wallet_sign_commit\",...}])"
|
||||
;;
|
||||
|
||||
sign)
|
||||
echo "Signing commit with Web3 identity..."
|
||||
echo " (Coming in WalletPress v1.2 — automated EIP-191 commit signing)"
|
||||
;;
|
||||
|
||||
verify)
|
||||
echo "Verifying Web3 commit signatures..."
|
||||
echo " (Coming in WalletPress v1.2 — signature verification from chain)"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 [setup|sign|verify]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
15
walletpress-mcp/pyproject.toml
Normal file
15
walletpress-mcp/pyproject.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[project]
|
||||
name = "walletpress-mcp"
|
||||
version = "1.0.0"
|
||||
description = "Standalone MCP Server for WalletPress — connect any MCP client to your wallet engine"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"mcp>=1.0.0",
|
||||
"httpx>=0.27.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
walletpress-mcp = "walletpress_mcp:main"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["walletpress_mcp*"]
|
||||
217
walletpress-mcp/walletpress_mcp.py
Normal file
217
walletpress-mcp/walletpress_mcp.py
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
"""walletpress-mcp — Standalone MCP Server for WalletPress.
|
||||
|
||||
Connects to any WalletPress backend and exposes all wallet operations
|
||||
as MCP tools. Works with any MCP client: opencode, Claude Code, Cursor,
|
||||
Kilo Code, VS Code, and more.
|
||||
|
||||
Two modes:
|
||||
1. REMOTE — connects to a WalletPress API server (api_url + api_key)
|
||||
2. LOCAL — imports wallet engine directly (must be on same machine)
|
||||
|
||||
Quick start:
|
||||
# Remote mode (connect to existing WalletPress instance)
|
||||
walletpress-mcp --api-url http://localhost:8010 --api-key wp_...
|
||||
|
||||
# Local mode (embed in WalletPress backend)
|
||||
python -m walletpress_mcp
|
||||
|
||||
# Connect from any MCP client:
|
||||
# opencode → Add MCP server: "walletpress-mcp"
|
||||
# Claude Code → mcp add walletpress-mcp
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
logger = logging.getLogger("wp.mcp.standalone")
|
||||
mcp = FastMCP("WalletPress Agent", log_level="WARNING")
|
||||
|
||||
|
||||
# ── Try local mode first (direct import) ──────────────────────────────────────
|
||||
BACKEND_URL = os.getenv("WP_API_URL", "")
|
||||
BACKEND_KEY = os.getenv("WP_API_KEY", "")
|
||||
|
||||
if BACKEND_URL and BACKEND_KEY:
|
||||
# Remote mode — proxy via REST API
|
||||
_mode = "remote"
|
||||
logger.info(f"Remote mode: {BACKEND_URL}")
|
||||
else:
|
||||
# Local mode — import wallet engine
|
||||
_mode = "local"
|
||||
logger.info("Local mode: importing wallet engine")
|
||||
import sys
|
||||
backend_path = os.getenv("WALLETPRESS_BACKEND", "")
|
||||
if backend_path:
|
||||
sys.path.insert(0, backend_path)
|
||||
|
||||
|
||||
def _call_backend(method: str, path: str, data: dict | None = None) -> dict:
|
||||
"""Call the WalletPress backend (remote or local)."""
|
||||
if _mode == "remote":
|
||||
import httpx
|
||||
headers = {"X-API-Key": BACKEND_KEY, "Content-Type": "application/json"}
|
||||
url = f"{BACKEND_URL.rstrip('/')}{path}"
|
||||
if method == "GET":
|
||||
r = httpx.get(url, headers=headers, timeout=15)
|
||||
else:
|
||||
r = httpx.post(url, headers=headers, json=data or {}, timeout=15)
|
||||
if r.status_code >= 400:
|
||||
return {"error": f"Backend {r.status_code}: {r.text[:200]}"}
|
||||
return r.json()
|
||||
else:
|
||||
# Local — import and call directly
|
||||
try:
|
||||
from core.vault import get_vault
|
||||
from wallet_engine.chains import CHAINS
|
||||
from wallet_engine.generator import get_generator as get_gen
|
||||
from core.config import cfg
|
||||
except ImportError:
|
||||
return {"error": "Cannot import wallet engine. Set WALLETPRESS_BACKEND or use WP_API_URL."}
|
||||
return _local_call(method, path, data)
|
||||
|
||||
|
||||
def _local_call(method: str, path: str, data: dict | None = None) -> dict:
|
||||
"""Handle a local call by routing to the appropriate module."""
|
||||
from core.vault import get_vault, WalletEntry
|
||||
from wallet_engine.chains import CHAINS
|
||||
from wallet_engine.generator import get_generator as get_gen
|
||||
from core.config import cfg
|
||||
import time, secrets
|
||||
|
||||
vault = get_vault()
|
||||
gen = get_gen(cfg.vault_password)
|
||||
|
||||
if path == "/health":
|
||||
return {"status": "ok", "mode": "local", "service": "walletpress-mcp"}
|
||||
if "/api/v1/chain-vault/chains" in path:
|
||||
return {"total_chains": len(CHAINS), "chains": {k: {"name": v.name, "symbol": v.symbol} for k, v in CHAINS.items()}}
|
||||
if "/api/v1/chain-vault/vault" in path and method == "GET":
|
||||
wallets = vault.list(limit=100)
|
||||
return {"wallets": [{"id": w.id, "chain": w.chain, "address": w.address, "label": w.label} for w in wallets]}
|
||||
if "/api/v1/chain-vault/stats" in path:
|
||||
return vault.stats()
|
||||
if "/api/v1/chain-vault/healthz" in path:
|
||||
return {"status": "ok"}
|
||||
return {"error": f"No local handler for {method} {path}"}
|
||||
|
||||
|
||||
# ── MCP Tool Definitions ─────────────────────────────────────────────────────
|
||||
|
||||
@mcp.tool()
|
||||
def vault_list(chain: str = "", limit: int = 50) -> list[dict]:
|
||||
"""List wallets in the vault."""
|
||||
result = _call_backend("GET", f"/api/v1/chain-vault/vault?chain={chain}&limit={limit}")
|
||||
return result.get("wallets", result) if isinstance(result, dict) else result
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def vault_stats() -> dict:
|
||||
"""Get vault statistics."""
|
||||
return _call_backend("GET", "/api/v1/chain-vault/stats")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def chains_list() -> dict:
|
||||
"""List all supported chains."""
|
||||
return _call_backend("GET", "/api/v1/chain-vault/chains")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def wallet_generate(chain: str, label: str = "", count: int = 1) -> dict:
|
||||
"""Generate wallets."""
|
||||
return _call_backend("POST", "/api/v1/chain-vault/generate", {"chain": chain, "label": label, "count": count})
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def wallet_delete(wallet_id: str) -> dict:
|
||||
"""Delete a wallet."""
|
||||
return _call_backend("DELETE", f"/api/v1/chain-vault/vault/{wallet_id}")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def balance_check(chain: str, address: str) -> dict:
|
||||
"""Check wallet balance."""
|
||||
return _call_backend("GET", f"/api/v1/chain-vault/validate/{chain}/{address}")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def validate_address(chain: str, address: str) -> dict:
|
||||
"""Validate an address format."""
|
||||
from wallet_engine.chains import CHAINS, validate_address as va
|
||||
chain_info = CHAINS.get(chain.lower())
|
||||
if not chain_info:
|
||||
return {"error": f"Unsupported chain: {chain}"}
|
||||
valid = va(chain, address)
|
||||
return {"chain": chain, "address": address, "valid": valid}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def health() -> dict:
|
||||
"""Check if the backend is connected."""
|
||||
return _call_backend("GET", "/health")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def plugins_list() -> list[dict]:
|
||||
"""List all registered protocol plugins."""
|
||||
try:
|
||||
from plugins.sdk import list_plugins
|
||||
return list_plugins()
|
||||
except ImportError:
|
||||
return [{"note": "Plugin system not available in standalone mode"}]
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def plugin_execute(plugin: str, tool: str, params: str) -> dict:
|
||||
"""Execute a plugin tool. Params should be JSON string."""
|
||||
try:
|
||||
from plugins.sdk import execute_plugin_tool
|
||||
import asyncio
|
||||
p = json.loads(params) if isinstance(params, str) else params
|
||||
return asyncio.run(execute_plugin_tool(plugin, tool, p))
|
||||
except ImportError:
|
||||
return {"error": "Plugin system not available"}
|
||||
|
||||
|
||||
# ── CLI Entry Point ──────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
"""Run the MCP server as a standalone CLI."""
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description="WalletPress MCP Server")
|
||||
parser.add_argument("--api-url", help="WalletPress API URL (remote mode)")
|
||||
parser.add_argument("--api-key", help="WalletPress API key")
|
||||
parser.add_argument("--backend-path", help="Path to walletpress backend (local mode)")
|
||||
parser.add_argument("--transport", default="stdio", choices=["stdio", "sse"], help="MCP transport")
|
||||
parser.add_argument("--port", type=int, default=8080, help="Port for SSE transport")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.api_url:
|
||||
os.environ["WP_API_URL"] = args.api_url
|
||||
if args.api_key:
|
||||
os.environ["WP_API_KEY"] = args.api_key
|
||||
if args.backend_path:
|
||||
os.environ["WALLETPRESS_BACKEND"] = args.backend_path
|
||||
|
||||
# Re-initialize with CLI args
|
||||
global BACKEND_URL, BACKEND_KEY, _mode
|
||||
BACKEND_URL = os.getenv("WP_API_URL", "")
|
||||
BACKEND_KEY = os.getenv("WP_API_KEY", "")
|
||||
_mode = "remote" if BACKEND_URL and BACKEND_KEY else "local"
|
||||
|
||||
if args.transport == "sse":
|
||||
import uvicorn
|
||||
app = mcp.sse_app()
|
||||
uvicorn.run(app, host="0.0.0.0", port=args.port)
|
||||
else:
|
||||
mcp.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue