MCP Server Setup Guide
Complete guide to setting up and configuring SEI MCP servers for AI agents
SEI MCP Server Setup Guide
This guide provides comprehensive instructions for setting up, configuring, and deploying SEI MCP (Model Context Protocol) servers for AI agent integration.
Overview
The SEI MCP server acts as a bridge between AI clients and the SEI blockchain, providing standardized tools for blockchain interactions. This setup guide covers:
- Server architecture and components
- Installation and configuration
- Environment setup and security
- Deployment options
- Monitoring and maintenance
Prerequisites
Before setting up your SEI MCP server, ensure you have:
- Node.js 18+ installed
- TypeScript knowledge (basic)
- SEI wallet with testnet tokens
- API keys for external services (optional)
Installation
1. Create Project Structure
# Create project directory
mkdir sei-mcp-server
cd sei-mcp-server
# Initialize package.json
npm init -y
# Install dependencies
npm install @axiomkit/sei @modelcontextprotocol/sdk
npm install viem @sei-js/evm zod
npm install dotenv
# Install dev dependencies
npm install -D typescript @types/node tsx nodemon
npm install -D @types/node-fetch
2. Project Structure
sei-mcp-server/
├── src/
│ ├── server.ts # Main server file
│ ├── tools/ # Tool implementations
│ │ ├── balance.ts
│ │ ├── transfer.ts
│ │ ├── tokens.ts
│ │ └── index.ts
│ ├── config/ # Configuration
│ │ ├── env.ts
│ │ └── chains.ts
│ ├── utils/ # Utilities
│ │ ├── logger.ts
│ │ ├── validation.ts
│ │ └── errors.ts
│ └── types/ # Type definitions
│ └── index.ts
├── .env.example # Environment template
├── .env # Environment variables
├── package.json
├── tsconfig.json
└── README.md
3. TypeScript Configuration
Create tsconfig.json
:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowJs": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Configuration
1. Environment Variables
Create .env.example
:
# SEI Configuration
SEI_RPC_URL=https://evm-rpc.sei-apis.com/
SEI_PRIVATE_KEY=0x... # Your private key (with 0x prefix)
# Server Configuration
SERVER_NAME=sei-blockchain-server
SERVER_VERSION=1.0.0
LOG_LEVEL=info
# Optional: External Services
DEXSCREENER_API_KEY=your_api_key_here
COINGECKO_API_KEY=your_api_key_here
# Optional: Rate Limiting
RATE_LIMIT_REQUESTS=100
RATE_LIMIT_WINDOW_MS=60000
# Optional: Caching
CACHE_TTL_MS=300000
Create your .env
file:
cp .env.example .env
# Edit .env with your actual values
2. Environment Validation
Create src/config/env.ts
:
import { z } from "zod";
import { config } from "dotenv";
// Load environment variables
config();
// Define environment schema
const envSchema = z.object({
// Required
SEI_RPC_URL: z.string().min(1, "SEI_RPC_URL is required"),
SEI_PRIVATE_KEY: z.string().min(1, "SEI_PRIVATE_KEY is required"),
// Optional with defaults
SERVER_NAME: z.string().default("sei-blockchain-server"),
SERVER_VERSION: z.string().default("1.0.0"),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
// Optional external services
DEXSCREENER_API_KEY: z.string().optional(),
COINGECKO_API_KEY: z.string().optional(),
// Optional rate limiting
RATE_LIMIT_REQUESTS: z.string().transform(Number).default("100"),
RATE_LIMIT_WINDOW_MS: z.string().transform(Number).default("60000"),
// Optional caching
CACHE_TTL_MS: z.string().transform(Number).default("300000"),
});
// Validate and export environment
export const env = envSchema.parse(process.env);
// Type for environment
export type Env = z.infer<typeof envSchema>;
3. Chain Configuration
Create src/config/chains.ts
:
import { defineChain } from "viem";
export const seiMainnet = defineChain({
id: 1329,
name: "SEI Mainnet",
network: "sei",
nativeCurrency: {
decimals: 18,
name: "SEI",
symbol: "SEI",
},
rpcUrls: {
default: {
http: ["https://evm-rpc.sei-apis.com/"],
},
public: {
http: ["https://evm-rpc.sei-apis.com/"],
},
},
blockExplorers: {
default: {
name: "SeiTrace",
url: "https://seitrace.com",
},
},
});
export const seiTestnet = defineChain({
id: 1328,
name: "SEI Testnet",
network: "sei-testnet",
nativeCurrency: {
decimals: 18,
name: "SEI",
symbol: "SEI",
},
rpcUrls: {
default: {
http: ["https://evm-rpc-testnet.sei-apis.com/"],
},
public: {
http: ["https://evm-rpc-testnet.sei-apis.com/"],
},
},
blockExplorers: {
default: {
name: "SeiTrace Testnet",
url: "https://testnet.seitrace.com",
},
},
});
export function getChainConfig(chainId?: number) {
switch (chainId) {
case 1329:
return seiMainnet;
case 1328:
return seiTestnet;
default:
return seiMainnet;
}
}
Core Server Implementation
1. Main Server File
Create src/server.ts
:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { AxiomSeiWallet } from "@axiomkit/sei";
import { env } from "./config/env.js";
import { getChainConfig } from "./config/chains.js";
import { createLogger } from "./utils/logger.js";
import { setupTools } from "./tools/index.js";
import { handleError } from "./utils/errors.js";
// Create logger
const logger = createLogger(env.LOG_LEVEL);
// Initialize SEI wallet
const wallet = new AxiomSeiWallet({
rpcUrl: env.SEI_RPC_URL,
privateKey: env.SEI_PRIVATE_KEY as `0x${string}`,
chain: getChainConfig(),
});
// Create MCP server
const server = new McpServer(
{
name: env.SERVER_NAME,
version: env.SERVER_VERSION,
},
{
capabilities: {
tools: {},
},
}
);
// Setup tools
setupTools(server, wallet, logger);
// Error handling
server.onerror = (error) => {
logger.error("Server error:", error);
};
// Start server
async function main() {
try {
logger.info("Starting SEI MCP Server...");
logger.info(`Wallet Address: ${wallet.walletAdress}`);
logger.info(`RPC URL: ${env.SEI_RPC_URL}`);
const transport = new StdioServerTransport();
await server.connect(transport);
logger.info("SEI MCP Server running on stdio");
} catch (error) {
logger.error("Failed to start server:", error);
process.exit(1);
}
}
// Handle graceful shutdown
process.on("SIGINT", async () => {
logger.info("Shutting down server...");
process.exit(0);
});
process.on("SIGTERM", async () => {
logger.info("Shutting down server...");
process.exit(0);
});
// Start the server
main().catch((error) => {
logger.error("Unhandled error:", error);
process.exit(1);
});
2. Logger Utility
Create src/utils/logger.ts
:
export type LogLevel = "debug" | "info" | "warn" | "error";
export interface Logger {
debug(message: string, meta?: any): void;
info(message: string, meta?: any): void;
warn(message: string, meta?: any): void;
error(message: string, meta?: any): void;
}
export function createLogger(level: LogLevel): Logger {
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
const currentLevel = levels[level];
const log = (level: LogLevel, message: string, meta?: any) => {
if (levels[level] >= currentLevel) {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level: level.toUpperCase(),
message,
...(meta && { meta }),
};
// Use stderr for all logging to avoid interfering with MCP protocol
console.error(JSON.stringify(logEntry));
}
};
return {
debug: (message: string, meta?: any) => log("debug", message, meta),
info: (message: string, meta?: any) => log("info", message, meta),
warn: (message: string, meta?: any) => log("warn", message, meta),
error: (message: string, meta?: any) => log("error", message, meta),
};
}
3. Error Handling
Create src/utils/errors.ts
:
export class SeiMcpError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500
) {
super(message);
this.name = "SeiMcpError";
}
}
export class ValidationError extends SeiMcpError {
constructor(message: string) {
super(message, "VALIDATION_ERROR", 400);
}
}
export class BlockchainError extends SeiMcpError {
constructor(message: string) {
super(message, "BLOCKCHAIN_ERROR", 502);
}
}
export class InsufficientBalanceError extends SeiMcpError {
constructor(message: string) {
super(message, "INSUFFICIENT_BALANCE", 400);
}
}
export function handleError(error: unknown): {
content: Array<{ type: string; text: string }>;
isError: boolean;
} {
if (error instanceof SeiMcpError) {
return {
content: [
{
type: "text",
text: `Error (${error.code}): ${error.message}`,
},
],
isError: true,
};
}
if (error instanceof Error) {
return {
content: [
{
type: "text",
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
return {
content: [
{
type: "text",
text: "An unknown error occurred",
},
],
isError: true,
};
}
Tool Implementation
1. Tool Index
Create src/tools/index.ts
:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { AxiomSeiWallet } from "@axiomkit/sei";
import { Logger } from "../utils/logger.js";
import { setupBalanceTools } from "./balance.js";
import { setupTransferTools } from "./transfer.js";
import { setupTokenTools } from "./tokens.js";
export function setupTools(
server: McpServer,
wallet: AxiomSeiWallet,
logger: Logger
) {
// Setup tool list
server.setRequestHandler("tools/list", async () => ({
tools: [
// Balance tools
{
name: "get_balance",
description: "Get SEI or ERC-20 token balance",
inputSchema: {
type: "object",
properties: {
token: {
type: "string",
description: "Token ticker symbol (e.g., 'SEI', 'USDC'). Leave empty for native SEI",
default: "SEI",
},
},
},
},
{
name: "get_wallet_info",
description: "Get wallet address and SEI balance",
inputSchema: {
type: "object",
properties: {},
},
},
// Transfer tools
{
name: "transfer_tokens",
description: "Transfer SEI or ERC-20 tokens",
inputSchema: {
type: "object",
properties: {
amount: {
type: "string",
description: "Amount to transfer (e.g., '1.5')",
},
recipient: {
type: "string",
description: "Recipient wallet address",
},
token: {
type: "string",
description: "Token ticker symbol (default: SEI)",
default: "SEI",
},
},
required: ["amount", "recipient"],
},
},
// Token tools
{
name: "get_token_address",
description: "Get token contract address from ticker symbol",
inputSchema: {
type: "object",
properties: {
ticker: {
type: "string",
description: "Token ticker symbol (e.g., 'USDC', 'WETH')",
},
},
required: ["ticker"],
},
},
{
name: "search_tokens",
description: "Search for tokens by name or symbol",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query (token name or symbol)",
},
limit: {
type: "number",
description: "Maximum number of results (default: 10)",
default: 10,
},
},
required: ["query"],
},
},
],
}));
// Setup tool handlers
server.setRequestHandler("tools/call", async (request) => {
const { name, arguments: args } = request.params;
logger.info(`Tool call: ${name}`, { args });
try {
// Route to appropriate handler
switch (name) {
case "get_balance":
case "get_wallet_info":
return await setupBalanceTools(server, wallet, logger);
case "transfer_tokens":
return await setupTransferTools(server, wallet, logger);
case "get_token_address":
case "search_tokens":
return await setupTokenTools(server, wallet, logger);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
logger.error(`Tool call failed: ${name}`, error);
return handleError(error);
}
});
}
2. Balance Tools
Create src/tools/balance.ts
:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { AxiomSeiWallet } from "@axiomkit/sei";
import { Logger } from "../utils/logger.js";
import { handleError } from "../utils/errors.js";
export async function setupBalanceTools(
server: McpServer,
wallet: AxiomSeiWallet,
logger: Logger
) {
// This would be called from the main tool handler
// Implementation details for balance-related tools
}
// Individual tool implementations
export async function getBalance(
wallet: AxiomSeiWallet,
token?: string
): Promise<{ content: Array<{ type: string; text: string }> }> {
try {
if (!token || token === "SEI") {
const balance = await wallet.getERC20Balance();
return {
content: [
{
type: "text",
text: `Your SEI balance is: ${balance} SEI`,
},
],
};
}
const tokenAddress = await wallet.getTokenAddressFromTicker(token);
if (!tokenAddress) {
return {
content: [
{
type: "text",
text: `Token '${token}' not found on SEI`,
},
],
};
}
const balance = await wallet.getERC20Balance(tokenAddress);
return {
content: [
{
type: "text",
text: `Your ${token} balance is: ${balance} ${token}`,
},
],
};
} catch (error) {
return handleError(error);
}
}
export async function getWalletInfo(
wallet: AxiomSeiWallet
): Promise<{ content: Array<{ type: string; text: string }> }> {
try {
const balance = await wallet.getERC20Balance();
return {
content: [
{
type: "text",
text: `Wallet Address: ${wallet.walletAdress}\nSEI Balance: ${balance} SEI`,
},
],
};
} catch (error) {
return handleError(error);
}
}
Package Scripts
Update package.json
with useful scripts:
{
"scripts": {
"build": "tsc",
"start": "node dist/server.js",
"dev": "tsx src/server.ts",
"dev:watch": "nodemon --exec tsx src/server.ts",
"clean": "rm -rf dist",
"type-check": "tsc --noEmit",
"lint": "eslint src/**/*.ts",
"test": "vitest"
}
}
Deployment Options
1. Local Development
# Development with hot reload
npm run dev:watch
# Production build
npm run build
npm start
2. Docker Deployment
Create Dockerfile
:
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
RUN npm ci --only=production
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Expose port (if using HTTP transport)
EXPOSE 3000
# Start the server
CMD ["npm", "start"]
Create docker-compose.yml
:
version: '3.8'
services:
sei-mcp-server:
build: .
environment:
- SEI_RPC_URL=${SEI_RPC_URL}
- SEI_PRIVATE_KEY=${SEI_PRIVATE_KEY}
- LOG_LEVEL=${LOG_LEVEL:-info}
volumes:
- ./logs:/app/logs
restart: unless-stopped
3. Systemd Service
Create /etc/systemd/system/sei-mcp-server.service
:
[Unit]
Description=SEI MCP Server
After=network.target
[Service]
Type=simple
User=sei-mcp
WorkingDirectory=/opt/sei-mcp-server
ExecStart=/usr/bin/node dist/server.js
Restart=always
RestartSec=10
Environment=NODE_ENV=production
Environment=SEI_RPC_URL=https://evm-rpc.sei-apis.com/
Environment=SEI_PRIVATE_KEY=your_private_key_here
[Install]
WantedBy=multi-user.target
Monitoring and Maintenance
1. Health Checks
Add health check endpoint:
// Add to server setup
server.setRequestHandler("ping", async () => {
try {
// Test wallet connection
await wallet.getERC20Balance();
return {
status: "healthy",
timestamp: new Date().toISOString(),
wallet: wallet.walletAdress,
};
} catch (error) {
return {
status: "unhealthy",
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : "Unknown error",
};
}
});
2. Logging and Metrics
// Add metrics collection
const metrics = {
requests: 0,
errors: 0,
lastRequest: null as Date | null,
};
// Update metrics in tool handlers
server.setRequestHandler("tools/call", async (request) => {
metrics.requests++;
metrics.lastRequest = new Date();
try {
// ... tool handling
} catch (error) {
metrics.errors++;
throw error;
}
});
3. Backup and Recovery
#!/bin/bash
# backup.sh - Backup server configuration and logs
BACKUP_DIR="/backup/sei-mcp-server"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR/$DATE"
# Backup configuration
cp .env "$BACKUP_DIR/$DATE/"
cp package.json "$BACKUP_DIR/$DATE/"
# Backup logs
cp -r logs/ "$BACKUP_DIR/$DATE/" 2>/dev/null || true
# Cleanup old backups (keep last 7 days)
find "$BACKUP_DIR" -type d -mtime +7 -exec rm -rf {} \;
Security Best Practices
1. Private Key Management
// Use environment variables
const privateKey = process.env.SEI_PRIVATE_KEY;
// Validate private key format
if (!privateKey?.startsWith("0x") || privateKey.length !== 66) {
throw new Error("Invalid private key format");
}
// Never log private keys
logger.info("Wallet initialized", {
address: wallet.walletAdress,
// Never include privateKey here
});
2. Input Validation
import { z } from "zod";
const addressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/);
const amountSchema = z.string().regex(/^\d+(\.\d+)?$/);
export function validateTransferInput(input: any) {
const schema = z.object({
amount: amountSchema,
recipient: addressSchema,
token: z.string().optional(),
});
return schema.parse(input);
}
3. Rate Limiting
import { RateLimiter } from "./utils/rate-limiter.js";
const rateLimiter = new RateLimiter({
requests: env.RATE_LIMIT_REQUESTS,
windowMs: env.RATE_LIMIT_WINDOW_MS,
});
// Apply rate limiting to tool calls
server.setRequestHandler("tools/call", async (request) => {
const clientId = request.params.name; // Use tool name as client ID
if (!rateLimiter.allow(clientId)) {
throw new Error("Rate limit exceeded");
}
// ... handle tool call
});
Troubleshooting
Common Issues
1. Server won't start
- Check environment variables
- Verify private key format
- Ensure RPC URL is accessible
2. Tool calls failing
- Check wallet balance
- Verify token addresses
- Review error logs
3. Performance issues
- Monitor RPC endpoint latency
- Implement caching
- Use connection pooling
Debug Mode
# Enable debug logging
LOG_LEVEL=debug npm run dev
# Check server health
curl -X POST http://localhost:3000/ping
This comprehensive setup guide provides everything needed to deploy and maintain a production-ready SEI MCP server. The modular architecture makes it easy to extend with additional tools and features as needed.