Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Binary file modified .yarn/install-state.gz
Binary file not shown.
11 changes: 10 additions & 1 deletion packages/mcp-server/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# Sei MCP Server Environment Variables

# Wallet Configuration
# Wallet mode: private-key | dynamic | porto | disabled
# - disabled: Disable all wallet functionality (default - safest for production)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, how will this work for dynamic and porto modes?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It extends that same WalletProvider interface and adds a new wallet mode "dynamic"

# - private-key: Use PRIVATE_KEY environment variable
WALLET_MODE=disabled

@dssei dssei Jul 1, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we assume we are in readonly mode if no private key is set?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does default to disabled if no private key. We could make it not required for the wallet mode, but with dynamic added we will still need a wallet mode for that.


# Private key for blockchain transactions (without 0x prefix)
# This is used when no private key is provided in the request
# Used when WALLET_MODE=private-key
# SECURITY: Never commit your actual private key to version control
PRIVATE_KEY=your_private_key_here

# Wallet API Key (future use with wallet providers)
# WALLET_API_KEY=your_wallet_api_key_here
22 changes: 22 additions & 0 deletions packages/mcp-server/bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@ const require = createRequire(import.meta.url);
const args = process.argv.slice(2);
const httpMode = args.includes('--http') || args.includes('-h');

// Parse wallet mode argument
let walletMode = null;
const walletModeIndex = args.findIndex(arg => arg === '--wallet-mode');
if (walletModeIndex !== -1 && walletModeIndex + 1 < args.length) {
walletMode = args[walletModeIndex + 1];
}

// Set environment variables based on CLI arguments
if (walletMode) {
const validModes = ['private-key', 'disabled'];
if (validModes.includes(walletMode)) {
process.env.WALLET_MODE = walletMode;
} else {
console.error(`Invalid wallet mode: ${walletMode}. Valid modes are: ${validModes.join(', ')}`);
process.exit(1);
}
}

// Determine which file to execute
const scriptPath = resolve(__dirname, '../dist/esm', httpMode ? '/server/http-server.js' : 'index.js');

Expand Down Expand Up @@ -45,6 +63,10 @@ try {
} catch (error) {
console.error('Error: Server files not found. The package may not be built correctly.');
console.error('Please try reinstalling the package or contact the maintainers.');
console.error('\nUsage:');
console.error(' mcp-server # Start in STDIO mode (default)');
console.error(' mcp-server --http # Start in HTTP mode');
console.error(' mcp-server --wallet-mode MODE # Set wallet mode (private-key|dynamic|porto|disabled)');
console.error(error);
process.exit(1);
}
2 changes: 2 additions & 0 deletions packages/mcp-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"cors": "^2.8.5",
"dotenv": "^16.5.0",
"express": "^4.21.2",
"jest": "^30.0.3",
"tsx": "^4.20.3",
"viem": "^2.30.5",
"zod": "^3.24.2"
},
Expand Down
27 changes: 25 additions & 2 deletions packages/mcp-server/src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import type { Hex } from 'viem';
// Load environment variables from .env file
dotenv.config();

// Wallet mode types
export type WalletMode = 'private-key' | 'disabled';

// Define environment variable schema
const envSchema = z.object({
PRIVATE_KEY: z.string().optional()
PRIVATE_KEY: z.string().optional(),
WALLET_MODE: z.enum(['private-key', 'disabled']).default('disabled'),
WALLET_API_KEY: z.string().optional() // Used for wallet providers
});

// Parse and validate environment variables
Expand All @@ -23,7 +28,9 @@ export const formatPrivateKey = (key?: string): string | undefined => {

// Export validated environment variables with formatted private key
export const config = {
privateKey: env.success ? formatPrivateKey(env.data.PRIVATE_KEY) : undefined
privateKey: env.success ? formatPrivateKey(env.data.PRIVATE_KEY) : undefined,
walletMode: (env.success ? env.data.WALLET_MODE : 'disabled') as WalletMode,
walletApiKey: env.success ? env.data.WALLET_API_KEY : undefined
};

/**
Expand All @@ -34,3 +41,19 @@ export const config = {
export function getPrivateKeyAsHex(): Hex | undefined {
return config.privateKey as Hex | undefined;
}

/**
* Check if wallet functionality is enabled based on configuration
* @returns True if wallet functionality should be available
*/
export function isWalletEnabled(): boolean {
return config.walletMode !== 'disabled';
}

/**
* Get the current wallet mode
* @returns The configured wallet mode
*/
export function getWalletMode(): WalletMode {
return config.walletMode;
}
26 changes: 9 additions & 17 deletions packages/mcp-server/src/core/services/clients.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { http, type Address, type Hex, type PublicClient, type WalletClient, createPublicClient, createWalletClient } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { DEFAULT_NETWORK, getChain, getRpcUrl } from '../chains.js';
import { getWalletProvider } from '../wallet/index.js';

// Cache for clients to avoid recreating them for each request
const clientCache = new Map<string, PublicClient>();
Expand Down Expand Up @@ -37,26 +38,17 @@ export function getPublicClient(network = DEFAULT_NETWORK): PublicClient {
}

/**
* Create a wallet client for a specific network and private key
* Get a wallet client using the configured wallet provider
*/
export function getWalletClient(privateKey: Hex, network = DEFAULT_NETWORK): WalletClient {
const chain = getChain(network);
const rpcUrl = getRpcUrl(network);
const account = privateKeyToAccount(privateKey);

return createWalletClient({
account,
chain,
transport: http(rpcUrl)
});
export async function getWalletClientFromProvider(network = DEFAULT_NETWORK): Promise<WalletClient> {
const walletProvider = getWalletProvider();
return walletProvider.getWalletClient(network);
}

/**
* Get an EVM address from a private key
* @param privateKey The private key in hex format (with or without 0x prefix)
* @returns The EVM address derived from the private key
* Get an EVM address from the configured wallet provider
*/
export function getAddressFromPrivateKey(privateKey: Hex): Address {
const account = privateKeyToAccount(privateKey);
return account.address;
export async function getAddressFromProvider(): Promise<Address> {
const walletProvider = getWalletProvider();
return walletProvider.getAddress();
}
6 changes: 3 additions & 3 deletions packages/mcp-server/src/core/services/contracts.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { GetLogsParameters, Hash, Hex, Log, ReadContractParameters, WriteContractParameters } from 'viem';
import { DEFAULT_NETWORK } from '../chains.js';
import { getPrivateKeyAsHex } from '../config.js';
import { getPublicClient, getWalletClient } from './clients.js';
import { getPublicClient, getWalletClientFromProvider } from './clients.js';
import * as services from './index.js';

/**
Expand All @@ -27,7 +27,7 @@ export async function writeContract(params: Record<string, any>, network = DEFAU
throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.');
}

const client = getWalletClient(key, network);
const client = await getWalletClientFromProvider(network);
return await client.writeContract(params as any);
}

Expand Down Expand Up @@ -75,7 +75,7 @@ export async function deployContract(
throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.');
}

const client = getWalletClient(key, network);
const client = await getWalletClientFromProvider(network);

if (!client.account) {
throw new Error('Wallet client account not available for contract deployment.');
Expand Down
59 changes: 12 additions & 47 deletions packages/mcp-server/src/core/services/transfer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { type Address, type Hash, type Hex, getContract, parseEther, parseUnits } from 'viem';
import { type Address, type Hash, getContract, parseEther, parseUnits } from 'viem';
import { DEFAULT_NETWORK } from '../chains.js';
import { getPrivateKeyAsHex } from '../config.js';
import { getPublicClient, getWalletClient } from './clients.js';
import { getPublicClient, getWalletClientFromProvider } from './clients.js';
import * as services from './index.js';

// Standard ERC20 ABI for transfers
Expand Down Expand Up @@ -119,14 +118,8 @@ export async function transferSei(
network = DEFAULT_NETWORK
): Promise<Hash> {
const validatedToAddress = services.helpers.validateAddress(toAddress);
// Get private key from environment
const privateKey = getPrivateKeyAsHex();

if (!privateKey) {
throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.');
}

const client = getWalletClient(privateKey, network);
// Get wallet client from provider
const client = await getWalletClientFromProvider(network);
const amountWei = parseEther(amount);

// Ensure account exists before using it
Expand Down Expand Up @@ -169,12 +162,8 @@ export async function transferERC20(
}> {
const validatedTokenAddress = services.helpers.validateAddress(tokenAddress);
const validatedToAddress = services.helpers.validateAddress(toAddress);
// Get private key from environment
const privateKey = getPrivateKeyAsHex();

if (!privateKey) {
throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.');
}
// Get wallet client from provider
const walletClient = await getWalletClientFromProvider(network);

const publicClient = getPublicClient(network);

Expand All @@ -192,9 +181,6 @@ export async function transferERC20(
// Parse the amount with the correct number of decimals
const rawAmount = parseUnits(amount, decimals);

// Create wallet client for sending the transaction
const walletClient = getWalletClient(privateKey, network);

// Ensure account exists before using it
if (!walletClient.account) {
throw new Error('Wallet account not initialized properly');
Expand Down Expand Up @@ -250,13 +236,8 @@ export async function approveERC20(
}> {
const validatedTokenAddress = services.helpers.validateAddress(tokenAddress);
const validatedSpenderAddress = services.helpers.validateAddress(spenderAddress);

// Get private key from environment
const privateKey = getPrivateKeyAsHex();

if (!privateKey) {
throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.');
}
// Get wallet client from provider
const walletClient = await getWalletClientFromProvider(network);

const publicClient = getPublicClient(network);
const contract = getContract({
Expand All @@ -272,9 +253,6 @@ export async function approveERC20(
// Parse the amount with the correct number of decimals
const rawAmount = parseUnits(amount, decimals);

// Create wallet client for sending the transaction
const walletClient = getWalletClient(privateKey, network);

// Ensure account exists before using it
if (!walletClient.account) {
throw new Error('Wallet account not initialized properly');
Expand Down Expand Up @@ -327,15 +305,9 @@ export async function transferERC721(
}> {
const validatedTokenAddress = services.helpers.validateAddress(tokenAddress);
const validatedToAddress = services.helpers.validateAddress(toAddress);
// Get private key from environment
const privateKey = getPrivateKeyAsHex();

if (!privateKey) {
throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.');
}

// Create wallet client for sending the transaction
const walletClient = getWalletClient(privateKey, network);
// Get wallet client from provider
const walletClient = await getWalletClientFromProvider(network);

// Ensure account exists before using it
if (!walletClient.account) {
Expand Down Expand Up @@ -405,15 +377,8 @@ export async function transferERC1155(
}> {
const validatedTokenAddress = services.helpers.validateAddress(tokenAddress);
const validatedToAddress = services.helpers.validateAddress(toAddress);
// Get private key from environment
const privateKey = getPrivateKeyAsHex();

if (!privateKey) {
throw new Error('Private key not available. Set the PRIVATE_KEY environment variable and restart the MCP server.');
}

// Create wallet client for sending the transaction
const walletClient = getWalletClient(privateKey, network);
// Get wallet client from provider
const walletClient = await getWalletClientFromProvider(network);

// Ensure account exists before using it
if (!walletClient.account) {
Expand Down
44 changes: 35 additions & 9 deletions packages/mcp-server/src/core/tools.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { Abi, Address, Chain, Hash, Hex, WriteContractParameters } from 'viem';
import type { Address, Hash, Hex } from 'viem';
import { z } from 'zod';
import { DEFAULT_NETWORK, getRpcUrl, getSupportedNetworks } from './chains.js';
import { getPrivateKeyAsHex } from './config.js';
import { isWalletEnabled, getWalletMode } from './config.js';
import { getWalletProvider } from './wallet';
import * as services from './services/index.js';

/**
Expand All @@ -11,6 +12,21 @@ import * as services from './services/index.js';
* @param server The MCP server instance
*/
export function registerEVMTools(server: McpServer) {
// Register read-only tools (always available)
registerReadOnlyTools(server);

// Register wallet-dependent tools (only if wallet is enabled)
if (isWalletEnabled()) {
registerWalletTools(server);
} else {
console.error('Wallet functionality is disabled. Wallet-dependent tools will not be available.');
}
}

/**
* Register read-only tools that don't require wallet functionality
*/
function registerReadOnlyTools(server: McpServer) {
// NETWORK INFORMATION TOOLS

// Get chain information
Expand Down Expand Up @@ -429,7 +445,12 @@ export function registerEVMTools(server: McpServer) {
}
}
);
}

/**
* Register wallet-dependent tools that require wallet functionality
*/
function registerWalletTools(server: McpServer) {
// TRANSFER TOOLS

// Transfer Sei
Expand Down Expand Up @@ -797,7 +818,7 @@ export function registerEVMTools(server: McpServer) {
// Parse ABI if it's a string
const parsedAbi = typeof abi === 'string' ? JSON.parse(abi) : abi;

const contractParams: Record<string, any> = {
const contractParams: Record<string, unknown> = {
address: contractAddress as Address,
abi: parsedAbi,
functionName,
Expand Down Expand Up @@ -843,7 +864,12 @@ export function registerEVMTools(server: McpServer) {
{
bytecode: z.string().describe("The compiled contract bytecode as a hex string (e.g., '0x608060405234801561001057600080fd5b50...')"),
abi: z.array(z.any()).describe('The contract ABI (Application Binary Interface) as a JSON array, needed for constructor function'),
args: z.array(z.any()).optional().describe("The constructor arguments to pass during deployment, as an array (e.g., ['param1', 'param2']). Leave empty if constructor has no parameters."),
args: z
.array(z.any())
.optional()
.describe(
"The constructor arguments to pass during deployment, as an array (e.g., ['param1', 'param2']). Leave empty if constructor has no parameters."
),
network: z.string().optional().describe("Network name (e.g., 'sei', 'sei-testnet', 'sei-devnet') or chain ID. Defaults to Sei mainnet.")
},
async ({ bytecode, abi, args = [], network = DEFAULT_NETWORK }) => {
Expand All @@ -852,7 +878,7 @@ export function registerEVMTools(server: McpServer) {
const parsedAbi = typeof abi === 'string' ? JSON.parse(abi) : abi;

// Ensure bytecode is a proper hex string
const formattedBytecode = bytecode.startsWith('0x') ? bytecode as Hex : `0x${bytecode}` as Hex;
const formattedBytecode = bytecode.startsWith('0x') ? (bytecode as Hex) : (`0x${bytecode}` as Hex);

const result = await services.deployContract(formattedBytecode, parsedAbi, args, network);

Expand Down Expand Up @@ -1294,20 +1320,20 @@ export function registerEVMTools(server: McpServer) {
async () => {
// Handler function starts here
try {
const privateKeyValue = getPrivateKeyAsHex();
if (!privateKeyValue) {
const walletProvider = getWalletProvider();
if (!walletProvider.isAvailable()) {
return {
content: [
{
type: 'text',
text: 'Error: The PRIVATE_KEY environment variable is not set. Please set this variable with your private key and restart the MCP server for this tool to function.'
text: `Error: Wallet provider '${walletProvider.getName()}' is not available. Please configure the wallet provider and restart the MCP server.`
}
],
isError: true
};
}

const address = services.getAddressFromPrivateKey(privateKeyValue);
const address = await services.getAddressFromProvider();

return {
content: [
Expand Down
Loading