mcp-wallet
Supports Ethereum (EVM) transactions through the user's own wallet: native transfers, ERC-20 token transfers, contract calls, message signing, and EIP-712 typed data, all approved in the user's browser wallet rather than with server-held keys.
Supports Optimism (EVM) transactions, letting an MCP tool request a native transfer, ERC-20 transfer, contract interaction, or signature that the user approves in their own wallet.
Supports Polygon (EVM) transactions, letting an MCP tool request a native transfer, ERC-20 transfer, contract interaction, or signature that the user approves in their own wallet.
Provides a Prisma storage adapter (bundled in the core package) for persisting pending wallet-approval requests in a database, with a ready-to-use schema model and indexes for sessionId, status, and expiry — intended for production, multi-request deployments.
Provides a Redis storage adapter (in the separate adapters package, with ioredis) so pending approval requests are shared across multiple server instances behind a load balancer, using SCAN-based expired-request cleanup that is safe on shared, multi-tenant Redis.
Supports Solana (and Solana Devnet) transactions, including native SOL and SPL token transfers, signed via the user's Phantom wallet through the same approval flow.
Connects to the user's existing browser wallet via WalletConnect (alongside MetaMask and Phantom) to display the approval URL, prompt the user to confirm, and return the resulting signed transaction or signature to the MCP tool.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-walletsend 0.05 ETH to 0x742d35Cc6634C0532925a3b844Bc454e4438f44e on Base"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-wallet
The missing bridge between remote MCP servers and Web3 wallets.
MCP servers run on your backend. User wallets (MetaMask, Phantom, WalletConnect) live in the browser. This package connects them — without ever touching private keys.
Claude → MCP tool → mcp-wallet → Approval URL → User's wallet → Signed txPackages
This is a monorepo — install only what you need:
Package | What it's for | Required? |
| Core server SDK — bridge, storage adapters (in-memory + Prisma), chain/signer helpers, on-chain verification | Yes |
| React | Only if you're building the approval page in React/Next.js |
| Extra storage adapters not bundled in core — currently Redis | Only if you need the Redis adapter (Prisma ships in core) |
Related MCP server: clearance-mcp
The Problem
Every existing approach forces a bad tradeoff:
Approach | Problem |
Private key in env | Agent has full unsupervised access to funds |
Custodial wallet (Coinbase AgentKit) | You own the keys, not the user |
| stdio only — breaks with remote/HTTP MCP |
Phantom MCP server | Phantom accounts only |
@wyntraxyz/mcp-wallet works with remote MCP over HTTP (the transport used by claude.ai connectors, Claude Desktop remote servers, and any production deployment) and supports any wallet the user already has.
How It Works
MCP tool is called → bridge creates a pending request in storage, returns an
approvalUrlUser opens the URL → sees a clear breakdown of what's being requested
User approves in their wallet → MetaMask / Phantom popup appears, they confirm
Your frontend calls
bridge.resolve()→ MCP tool can now confirm the action
No keys ever leave the user's wallet.
Install
# Core server SDK — always required
npm install @wyntraxyz/mcp-wallet
# React UI component — optional, skip if you're building your own approval UI
npm install @wyntraxyz/mcp-wallet-ui
# Extra storage adapters (Redis) — optional, skip if using in-memory or Prisma
npm install @wyntraxyz/mcp-wallet-adaptersQuick Start
1. Initialize the bridge (MCP server)
import { createWalletBridge } from "@wyntraxyz/mcp-wallet";
export const bridge = createWalletBridge({
approvalBaseUrl: "https://yourapp.xyz/wallet/approve",
// storage: new PrismaAdapter(prisma), // for production
ttl: 600, // 10 minutes
});2. Use it in any MCP tool
import { bridge, formatBridgeResult } from "@wyntraxyz/mcp-wallet";
// Inside your MCP tool handler:
server.tool("buy_product", async ({ productId, price }, ctx) => {
const pending = await bridge.requestSignature({
sessionId: ctx.user.id,
transaction: {
type: "transfer",
chain: "base",
to: "0xRecipient...",
value: price,
metadata: { productId, action: "buy_product" },
},
});
return formatBridgeResult(pending);
// Claude returns: "Please approve this transaction at: https://yourapp.xyz/wallet/approve/abc123"
});3. Add the approval page (Next.js)
// app/wallet/approve/[id]/page.tsx
"use client";
import { WalletApproval } from "@wyntraxyz/mcp-wallet-ui";
import { useSendTransaction } from "wagmi";
export default function ApprovePage({ params }: { params: { id: string } }) {
return (
<WalletApproval
requestId={params.id}
appName="Your App"
fetchRequest={async (id) => {
const res = await fetch(`/api/mcp-wallet/requests/${id}`);
return res.json();
}}
onApprove={async (request) => {
// Sign with wagmi / viem / @solana/web3.js
const hash = await sendTransaction({ ... });
// Tell the bridge
await fetch(`/api/mcp-wallet/resolve/${request.id}`, {
method: "POST",
body: JSON.stringify({ txHash: hash, signerAddress: address }),
});
return { txHash: hash, signerAddress: address };
}}
onReject={async (request) => {
await fetch(`/api/mcp-wallet/reject/${request.id}`, { method: "POST" });
}}
/>
);
}4. Add 3 API routes
// GET /api/mcp-wallet/requests/[id] → bridge.getRequest(id)
// POST /api/mcp-wallet/resolve/[id] → bridge.resolve(id, result)
// POST /api/mcp-wallet/reject/[id] → bridge.reject(id, reason)See docs/examples/nextjs-api-routes.ts for the full implementation.
Storage Adapters
In-Memory (default) — @wyntraxyz/mcp-wallet
Zero config. Good for development and single-process deployments.
const bridge = createWalletBridge({ approvalBaseUrl: "..." });Prisma (production) — @wyntraxyz/mcp-wallet
Ships in core — no extra install needed.
import { PrismaAdapter } from "@wyntraxyz/mcp-wallet/adapters/prisma";
import { prisma } from "./lib/prisma";
const bridge = createWalletBridge({
approvalBaseUrl: "...",
storage: new PrismaAdapter(prisma),
});Add to your schema.prisma:
model McpWalletRequest {
id String @id @default(cuid())
sessionId String
transaction Json
status String @default("pending")
approvalUrl String
createdAt DateTime @default(now())
expiresAt DateTime
resolvedAt DateTime?
result Json?
error String?
@@index([sessionId])
@@index([status, expiresAt])
}Redis (production, multi-process) — @wyntraxyz/mcp-wallet-adapters
Requires the separate adapters package plus ioredis:
npm install @wyntraxyz/mcp-wallet-adapters ioredisimport { createWalletBridge } from "@wyntraxyz/mcp-wallet";
import { RedisAdapter } from "@wyntraxyz/mcp-wallet-adapters/redis";
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
const bridge = createWalletBridge({
approvalBaseUrl: "...",
storage: new RedisAdapter(redis),
});Good fit when you're running multiple server instances behind a load balancer and want pending requests visible to all of them without a full SQL database. Expired-request cleanup uses SCAN rather than KEYS, so it's safe to run against a shared, multi-tenant Redis instance.
Custom Adapter
Implement the StorageAdapter interface to use any database:
import { StorageAdapter, PendingRequest } from "@wyntraxyz/mcp-wallet";
class MyAdapter implements StorageAdapter {
async create(req) { ... }
async findById(id) { ... }
async findBySession(sessionId) { ... }
async update(id, patch) { ... }
async delete(id) { ... }
async cleanup(olderThan?) { ... }
}Supported Chains
Chain | ID | Family |
Ethereum |
| EVM |
Base |
| EVM |
Base Sepolia |
| EVM |
Polygon |
| EVM |
Arbitrum |
| EVM |
Optimism |
| EVM |
Solana |
| Solana |
Solana Devnet |
| Solana |
Custom chains: pass any string as chain in your transaction — the bridge stores it and passes it through to the approval UI.
Transaction Types
// Native token transfer
{ type: "transfer", chain: "base", to: "0x...", value: "0.01" }
// ERC-20 / SPL token
{ type: "token_transfer", chain: "base", to: "0x...", tokenAddress: "0x...", amount: "100" }
// Contract interaction
{ type: "contract_call", chain: "base", to: "0x...", abi: [...], functionName: "mint", args: [] }
// Message signing (no on-chain tx)
{ type: "sign_message", chain: "ethereum", message: "Sign in to YourApp" }
// EIP-712 typed data
{ type: "sign_typed", chain: "ethereum", domain: {...}, types: {...}, value: {...} }Bridge API
// Create a pending request
bridge.requestSignature({ sessionId, transaction, ttl? })
→ { requestId, approvalUrl, expiresAt, status: "pending" }
// Get current status
bridge.getRequest(requestId)
→ PendingRequest | null
// Wait for resolution (long-polling)
bridge.waitForApproval(requestId, { pollInterval?, timeout? })
→ PendingRequest
// Resolve (called by your frontend after signing)
bridge.resolve(requestId, { txHash?, signature?, signerAddress })
→ PendingRequest
// Reject
bridge.reject(requestId, reason?)
→ PendingRequest
// List pending requests for a session
bridge.listPending(sessionId)
→ PendingRequest[]
// Clean up expired requests
bridge.cleanup()
→ number (count cleaned)Security: verify before you trust resolve()
bridge.resolve(requestId, result) is normally called from a public API
route (POST /api/mcp-wallet/resolve/:id). That route has no way to
know, on its own, whether result.txHash / result.signature actually
correspond to a real transaction or signature — the browser could send
anything. If you gate access, unlock paid content, or release funds when
a request resolves, an unverified resolve() lets anyone who knows a
requestId mark it as paid without moving any funds.
Pass verify to createWalletBridge() to close this:
import { createWalletBridge, createEvmVerifier } from "@wyntraxyz/mcp-wallet";
const bridge = createWalletBridge({
approvalBaseUrl: "https://yourapp.xyz/wallet/approve",
verify: createEvmVerifier({ publicClient }), // checks sender, recipient, amount on-chain
});createEvmVerifier fetches the transaction/receipt from your RPC (via a
viem PublicClient) and confirms it succeeded, that from matches the
claimed signerAddress, and that to/value match what was requested,
before resolve() is allowed to move the request out of "pending". For
sign_message / sign_typed, it recovers the signer from the signature
instead. createSolanaVerifier does the equivalent for Solana transfers;
sign_message on Solana needs you to supply verifyOffChainSignature
(e.g. with tweetnacl), since this package doesn't bundle an ed25519
verifier.
Without verify, resolve() logs a warning and falls back to trusting the
client-submitted result — fine for a local demo, not for production.
React Hooks
import {
useWalletBridgeRequest,
useBridgeApproval,
} from "@wyntraxyz/mcp-wallet-ui";
// Fetch + poll a request
const { request, loading, error, refetch } = useWalletBridgeRequest({
endpoint: "/api/mcp-wallet/requests",
requestId,
});
// Handle approve/reject actions
const { approve, reject, approving, phase } = useBridgeApproval({
resolveEndpoint: "/api/mcp-wallet/resolve",
rejectEndpoint: "/api/mcp-wallet/reject",
});Theming (@wyntraxyz/mcp-wallet-ui)
<WalletApproval /> reads its accent color from three CSS custom properties
— --accent, --accent2 (hover shade), --accent3 (darker gradient end) —
via var(--accent, #CCFF00) internally. That means it inherits your app's
theme automatically, with no config on your end:
1. Nothing to do, if you already set these globally. If your app's
global.css defines them on :root, any page that imports that stylesheet
— including your /wallet/approve/[id] route, since it shares your app's
root layout — will theme the component correctly. CSS custom properties
resolve through the DOM at render time, not through your file/package
structure, so it doesn't matter that mcp-wallet-ui lives in a separate
package from src/app.
/* src/app/global.css */
:root {
--accent: #CCFF00;
--accent2: #B8E600;
--accent3: #9ACC00;
}2. Per-instance override, e.g. for per-tenant branding, without touching global CSS:
<WalletApproval
theme={{ accent: "#FF6B00", accent2: "#E65F00", accent3: "#CC5400" }}
// ...
/>3. Do nothing at all and it falls back to the built-in lime theme
(#CCFF00 / #B8E600 / #9ACC00).
If a consumer outside Wyntrax installs this package with no global theme and
no theme prop, they get the lime default — it's not tied to your app's
CSS in any way that would break for them.
Apps Using This
Wyntrax — Web3 creator monetization platform
Add yours via PR.
Roadmap
WebSocket push (instead of polling)
WalletConnect v3 deep-link support
Transaction simulation preview (via Tenderly / Alchemy)
Session key support (pre-authorized, scoped spending)
React Native / mobile approval UI
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
A paid remote MCP for AI agent browser approval MCP, built to return verdicts, receipts, usage logs,
Hosted MCP server for AI agent identity, permissions, verification, and reusable proof.
OAuth scope approvals and consent receipts for remote MCP servers.
An authenticated remote MCP server for user-owned devices and one-shot capability invocation.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceNon-custodial MCP server that routes blockchain transactions to your browser wallet (MetaMask, Rabby, etc.) for signing — private keys never leave your browser.2MIT
- AlicenseNot gradedqualityCmaintenanceMCP server that enables AI agents to request human approval before spending money, check approval status, verify signed tokens, and manage API keys.13 npmMIT
- AlicenseNot gradedqualityAmaintenanceMCP server that lets any AI agent operate a wallet directly on-chain: create wallets, send, swap, bridge, and deploy contracts across EVM and Bitcoin networks.2MIT
- FlicenseNot gradedqualityDmaintenanceHosted remote MCP for AI agent browser approval. Provides structured tools for page approval workflows, session management, and audit receipts.-