@mcp-wallet/sdk
v0.1.0
Published
Extend the MCP SDK with on-chain transaction signing capabilities
Maintainers
Readme
mcp-wallet
Extend the MCP SDK with on-chain transaction signing. Give your AI agents a wallet.
mcp-wallet adds server.transaction() alongside the standard server.tool(). Transaction tools go through a full safety pipeline: build -> introspect -> policy -> simulate -> sign -> submit — with audit logging, spending tracking, and webhook notifications at every step.
Install
npm install @mcp-wallet/sdk @modelcontextprotocol/sdkFor the Solana adapter:
npm install @solana/web3.jsFor SQLite persistence:
npm install better-sqlite3Quick Start
import { SigningMcpServer, maxPerTransaction, maxPerDay } from '@mcp-wallet/sdk';
import { SolanaKeypairAdapter } from '@mcp-wallet/sdk/adapters/solana';
import { Keypair } from '@solana/web3.js';
import { z } from 'zod';
const server = new SigningMcpServer(
{ name: 'My Agent', version: '1.0.0' },
{
wallet: new SolanaKeypairAdapter({
keypair: Keypair.generate(),
rpcUrl: 'https://api.devnet.solana.com',
}),
policies: [
maxPerTransaction(500_000_000), // 0.5 SOL
maxPerDay(2_000_000_000), // 2 SOL
],
},
);
// Regular MCP tool — no signing
server.tool('get_price', 'Get token price', { token: z.string() }, async (params) => ({
content: [{ type: 'text', text: `Price: $1.00` }],
}));
// Transaction tool — full signing pipeline
server.transaction(
'transfer_sol',
'Transfer SOL to a recipient',
{
recipient: z.string().describe('Recipient address'),
amount: z.number().describe('Amount in lamports'),
},
async (params, extra) => {
const tx = buildTransferTx(params.recipient, params.amount, extra.signerPublicKey);
return {
transaction: tx.serialize({ requireAllSignatures: false }),
amount: params.amount as number,
recipient: params.recipient as string,
};
},
);Features
Policy Engine
Guard every transaction with composable rules:
import {
maxPerTransaction,
maxPerDay,
allowedRecipients,
allowedPrograms,
rateLimit,
replayProtection,
customPolicy,
} from '@mcp-wallet/sdk';
const server = new SigningMcpServer(info, {
wallet: adapter,
policies: [
maxPerTransaction(100_000_000), // max per tx
maxPerDay(1_000_000_000), // max per session
allowedRecipients(['addr1', 'addr2']), // whitelist
allowedPrograms(['TokenkegQ...']), // program whitelist
rateLimit(10, 60), // 10 calls per 60s
replayProtection(30), // block duplicate tx within 30s
customPolicy('my-rule', (ctx) => { // your own logic
return { allowed: true, policyName: 'my-rule' };
}),
],
});Policies read structured metadata from the handler's return value (amount, recipient, programs) — not from raw params.
Transaction Introspection
Verify that handler-declared metadata matches the actual transaction bytes:
import type { TransactionDecoder } from '@mcp-wallet/sdk';
const decoder: TransactionDecoder = {
chain: 'solana',
decode(tx) {
// Parse the raw transaction and extract facts
return { totalAmount: 100, recipients: ['addr'], programs: ['prog'] };
},
};
const server = new SigningMcpServer(info, {
wallet: adapter,
decoders: [decoder], // mismatches reject the transaction
});Dry-Run Mode
Preview what a transaction would do without signing:
const result = await server.dryRun('transfer_sol', {
recipient: 'addr',
amount: 500_000_000,
});
// result: { allowed, policyResults, simulationResult, estimatedFee }Multi-Wallet (Multi-Chain)
One server, multiple chains:
const server = new SigningMcpServer(info, {
wallets: [solanaAdapter, evmAdapter],
});
server.transaction('send_sol', 'Send SOL', schema, handler, { chain: 'solana' });
server.transaction('send_eth', 'Send ETH', schema, handler, { chain: 'evm' });Key Rotation
Hot-swap wallet adapters at runtime:
server.rotateWallet('solana', newSolanaAdapter);
// Existing tools pick up the new adapter on next callTransaction Batching
Multiple operations in one tool call:
server.batch('multi_send', 'Send to multiple recipients', schema,
async (params) => [
{ transaction: tx1, amount: 100, recipient: 'addr-1' },
{ transaction: tx2, amount: 200, recipient: 'addr-2' },
],
);Each sub-transaction goes through the full pipeline independently.
Persistence
Durable spending tracking and audit history:
import { SqlitePersistenceAdapter } from '@mcp-wallet/sdk/persistence/sqlite';
const server = new SigningMcpServer(info, {
wallet: adapter,
persistence: new SqlitePersistenceAdapter('./mcp-wallet.db'),
});Spending limits survive restarts. Audit history is queryable:
const history = await server.getAuditLog().queryHistory({
chain: 'solana',
since: new Date('2026-03-01'),
limit: 50,
});Implement PersistenceAdapter for your own storage (Postgres, Redis, etc.).
Chain Query Tools
Auto-register read-only tools from a query provider:
const server = new SigningMcpServer(info, {
wallet: adapter,
queryProviders: [{
chain: 'solana',
getBalance: async (addr) => ({ balance: 5_000_000_000, decimals: 9 }),
getTokenAccounts: async (addr) => [{ mint: 'USDC...', balance: 1000000 }],
getTransactionHistory: async (addr, limit) => [{ txHash: '...', timestamp: '...', status: 'confirmed' }],
}],
});
// Auto-creates: get_balance_solana, get_token_accounts_solana, get_transaction_history_solanaWebhook Notifications
Push signing events to external systems:
const server = new SigningMcpServer(info, {
wallet: adapter,
webhooks: [
{
url: 'https://hooks.slack.com/...',
events: ['signed', 'denied'], // or 'failed', 'all'
secret: 'hmac-key', // adds X-Signature-256 header
},
],
});Audit Logging
Every signing decision is recorded:
const server = new SigningMcpServer(info, {
wallet: adapter,
auditSink: (entry) => {
console.log(`[${entry.signed ? 'SIGNED' : 'DENIED'}] ${entry.toolName} ${entry.txHash ?? ''}`);
},
});Transport
SigningMcpServer extends McpServer — it works with any MCP transport:
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() });
await server.connect(transport);See examples/solana-signing-server for a complete Express setup.
Writing a Wallet Adapter
Implement the WalletAdapter interface for any chain:
import type { WalletAdapter, SimulationResult } from '@mcp-wallet/sdk';
class MyChainAdapter implements WalletAdapter {
readonly chain = 'my-chain';
readonly publicKey = '0x...';
async sign(transaction: Uint8Array): Promise<Uint8Array> { /* ... */ }
async simulate?(transaction: Uint8Array): Promise<SimulationResult> { /* ... */ }
async submit?(signedTransaction: Uint8Array): Promise<string> { /* ... */ }
}The SDK ships SolanaKeypairAdapter at @mcp-wallet/sdk/adapters/solana.
API Reference
SigningMcpServer
| Method | Description |
|---|---|
| transaction(name, desc, schema, handler, opts?) | Register a signing tool |
| batch(name, desc, schema, handler, opts?) | Register a batch signing tool |
| dryRun(toolName, params) | Preview a transaction without signing |
| rotateWallet(chain, newAdapter) | Hot-swap a wallet adapter |
| getAuditLog() | Access audit log and history |
| getSpendingTracker() | Access spending data |
| getWallet(chain) | Get a wallet adapter by chain |
| getSigningCapability() | Get advertised capabilities |
| tool(), resource(), prompt() | Standard MCP methods (inherited) |
Built-in Policies
| Policy | Description |
|---|---|
| maxPerTransaction(amount) | Max spend per transaction |
| maxPerDay(amount) | Max cumulative spend per session |
| allowedRecipients(addrs) | Recipient whitelist |
| allowedPrograms(addrs) | Program/contract whitelist |
| rateLimit(maxCalls, windowSec) | Call frequency limit |
| replayProtection(windowSec) | Block duplicate transactions |
| customPolicy(name, fn) | Your own policy logic |
License
MIT
