Documentation Index
Fetch the complete documentation index at: https://0g.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Development Guide
This guide covers how to develop, extend, and contribute to the MCP 0G Server.
Development Setup
Prerequisites
- Node.js 18.0 or higher
- TypeScript 4.9 or higher
- Git for version control
- Code editor (VS Code recommended)
Local Development
-
Clone Repository
git clone https://github.com/0gfoundation/mcp-0g.git
cd mcp-0g
-
Install Dependencies
-
Environment Setup
cp .env.example .env
# Edit .env with your configuration
-
Development Mode
This starts the server with auto-reload on file changes.
Project Structure
mcp-0g/
├── src/ # Source code
│ ├── tools/ # MCP tool implementations
│ ├── services/ # Business logic services
│ ├── utils/ # Utility functions
│ ├── types/ # TypeScript type definitions
│ └── index.ts # Main server entry point
├── docs/ # Documentation
├── examples/ # Usage examples
├── tests/ # Test files
├── dist/ # Compiled JavaScript
└── package.json # Project configuration
Architecture Overview
Core Components
MCP Server
The main server implements the Model Context Protocol specification:
// src/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new Server({
name: "0g-mcp-server",
version: "1.0.0"
}, {
capabilities: {
tools: {}
}
});
Tools are registered with the MCP server:
// Tool registration
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get_chain_info",
description: "Get 0G chain information",
inputSchema: {
type: "object",
properties: {}
}
}
// ... more tools
]
};
});
Service Layer
Business logic is organized into services:
// src/services/BlockchainService.ts
export class BlockchainService {
constructor(private provider: ethers.Provider) {}
async getChainInfo(): Promise<ChainInfo> {
const network = await this.provider.getNetwork();
const blockNumber = await this.provider.getBlockNumber();
return {
chainId: network.chainId,
blockNumber,
networkName: network.name
};
}
}
Each tool follows this pattern:
// src/tools/ExampleTool.ts
import { z } from 'zod';
// Input validation schema
export const ExampleToolSchema = z.object({
parameter1: z.string(),
parameter2: z.number().optional()
});
// Tool implementation
export async function exampleTool(
input: z.infer<typeof ExampleToolSchema>
): Promise<any> {
// Validate input
const validated = ExampleToolSchema.parse(input);
// Business logic
const result = await performOperation(validated);
return result;
}
// Tool metadata
export const exampleToolMetadata = {
name: "example_tool",
description: "Example tool description",
inputSchema: ExampleToolSchema.schema
};
Registration Process
-
Create Tool File
Create a new file in
src/tools/ following the naming convention.
-
Implement Tool Logic
// src/tools/NewTool.ts
import { z } from 'zod';
import { BlockchainService } from '../services/BlockchainService.js';
export const NewToolSchema = z.object({
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid address format")
});
export async function newTool(
input: z.infer<typeof NewToolSchema>,
blockchainService: BlockchainService
): Promise<any> {
const { address } = NewToolSchema.parse(input);
// Your logic here
const result = await blockchainService.someOperation(address);
return {
success: true,
data: result
};
}
export const newToolMetadata = {
name: "new_tool",
description: "Description of what this tool does",
inputSchema: NewToolSchema.schema
};
-
Register Tool
Add to the tool registry:
// src/index.ts
import { newTool, newToolMetadata } from './tools/NewTool.js';
// Add to tools list
const tools = [
// ... existing tools
newToolMetadata
];
// Add to call handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
// ... existing cases
case "new_tool":
return { content: [{ type: "text", text: JSON.stringify(await newTool(args, blockchainService)) }] };
default:
throw new McpError(ErrorCode.MethodNotFound, `Tool not found: ${name}`);
}
});
Always validate inputs using Zod schemas:
const ToolSchema = z.object({
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/),
amount: z.string().regex(/^\d+$/),
gasLimit: z.number().min(21000).optional()
});
Error Handling
Implement comprehensive error handling:
export async function toolFunction(input: any): Promise<any> {
try {
const validated = ToolSchema.parse(input);
const result = await performOperation(validated);
return { success: true, data: result };
} catch (error) {
if (error instanceof z.ZodError) {
throw new McpError(ErrorCode.InvalidParams, `Validation error: ${error.message}`);
}
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, error.message);
}
throw new McpError(ErrorCode.InternalError, "Unknown error occurred");
}
}
Async Operations
Handle async operations properly:
export async function asyncTool(input: any): Promise<any> {
const validated = ToolSchema.parse(input);
// Use Promise.all for parallel operations
const [balance, nonce] = await Promise.all([
provider.getBalance(validated.address),
provider.getTransactionCount(validated.address)
]);
return { balance: balance.toString(), nonce };
}
Service Development
Creating Services
Services encapsulate business logic:
// src/services/TokenService.ts
export class TokenService {
constructor(
private provider: ethers.Provider,
private signer?: ethers.Signer
) {}
async getTokenMetadata(tokenAddress: string): Promise<TokenMetadata> {
const contract = new ethers.Contract(tokenAddress, ERC20_ABI, this.provider);
const [name, symbol, decimals] = await Promise.all([
contract.name(),
contract.symbol(),
contract.decimals()
]);
return { name, symbol, decimals };
}
async transferToken(
tokenAddress: string,
to: string,
amount: string
): Promise<string> {
if (!this.signer) {
throw new Error("Signer required for token transfer");
}
const contract = new ethers.Contract(tokenAddress, ERC20_ABI, this.signer);
const tx = await contract.transfer(to, amount);
return tx.hash;
}
}
Service Integration
Integrate services with dependency injection:
// src/index.ts
const provider = new ethers.JsonRpcProvider(process.env.ZEROQ_RPC_URL);
const blockchainService = new BlockchainService(provider);
const tokenService = new TokenService(provider);
const walletService = new WalletService();
// Pass services to tools
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "get_token_metadata":
return {
content: [{
type: "text",
text: JSON.stringify(await getTokenMetadata(args, tokenService))
}]
};
// ... other cases
}
});
Testing
Unit Tests
Write unit tests for tools and services:
// tests/tools/GetBalance.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { getBalance } from '../../src/tools/GetBalance.js';
import { BlockchainService } from '../../src/services/BlockchainService.js';
describe('GetBalance Tool', () => {
let mockService: jest.Mocked<BlockchainService>;
beforeEach(() => {
mockService = {
getBalance: jest.fn()
} as any;
});
it('should return balance for valid address', async () => {
const mockBalance = '1000000000000000000'; // 1 ETH in wei
mockService.getBalance.mockResolvedValue(mockBalance);
const result = await getBalance(
{ address: '0x742d35Cc6634C0532925a3b8D4C9db96590c6C87' },
mockService
);
expect(result.balance).toBe(mockBalance);
expect(mockService.getBalance).toHaveBeenCalledWith('0x742d35Cc6634C0532925a3b8D4C9db96590c6C87');
});
it('should throw error for invalid address', async () => {
await expect(getBalance(
{ address: 'invalid-address' },
mockService
)).rejects.toThrow('Invalid address format');
});
});
Integration Tests
Test complete workflows:
// tests/integration/WalletFlow.test.ts
import { describe, it, expect } from 'vitest';
import { createWallet, getWallet } from '../../src/tools/WalletTools.js';
describe('Wallet Integration', () => {
it('should create and retrieve wallet', async () => {
const walletName = 'test-wallet';
const password = 'secure-password';
// Create wallet
const createResult = await createWallet({
name: walletName,
password
});
expect(createResult.success).toBe(true);
expect(createResult.address).toMatch(/^0x[a-fA-F0-9]{40}$/);
// Retrieve wallet
const getResult = await getWallet({
name: walletName
});
expect(getResult.name).toBe(walletName);
expect(getResult.address).toBe(createResult.address);
});
});
Running Tests
# Run all tests
npm test
# Run with coverage
npm run test:coverage
# Run specific test file
npm test -- GetBalance.test.ts
# Watch mode for development
npm run test:watch
Configuration Management
Environment Variables
Define configuration schema:
// src/config/index.ts
import { z } from 'zod';
const ConfigSchema = z.object({
ZEROQ_RPC_URL: z.string().url(),
RATE_LIMIT_REQUESTS: z.string().transform(Number).pipe(z.number().min(1)),
RATE_LIMIT_WINDOW: z.string().transform(Number).pipe(z.number().min(1000)),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info')
});
export const config = ConfigSchema.parse(process.env);
Feature Flags
Implement feature flags for gradual rollouts:
// src/config/features.ts
export const features = {
ENABLE_CONTRACT_DEPLOYMENT: process.env.ENABLE_CONTRACT_DEPLOYMENT === 'true',
ENABLE_BATCH_OPERATIONS: process.env.ENABLE_BATCH_OPERATIONS === 'true',
MAX_BATCH_SIZE: parseInt(process.env.MAX_BATCH_SIZE || '10')
};
Logging and Monitoring
Structured Logging
Implement structured logging:
// src/utils/logger.ts
import winston from 'winston';
export const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'logs/mcp-server.log' })
]
});
Metrics Collection
Add metrics for monitoring:
// src/utils/metrics.ts
export class Metrics {
private static toolCallCount = new Map<string, number>();
private static errorCount = new Map<string, number>();
static incrementToolCall(toolName: string): void {
const current = this.toolCallCount.get(toolName) || 0;
this.toolCallCount.set(toolName, current + 1);
}
static incrementError(errorType: string): void {
const current = this.errorCount.get(errorType) || 0;
this.errorCount.set(errorType, current + 1);
}
static getMetrics(): any {
return {
toolCalls: Object.fromEntries(this.toolCallCount),
errors: Object.fromEntries(this.errorCount)
};
}
}
Security Considerations
Always sanitize and validate inputs:
import { z } from 'zod';
import { ethers } from 'ethers';
const AddressSchema = z.string().refine((addr) => {
return ethers.isAddress(addr);
}, "Invalid Ethereum address");
const AmountSchema = z.string().refine((amount) => {
try {
ethers.parseEther(amount);
return true;
} catch {
return false;
}
}, "Invalid amount format");
Rate Limiting
Implement rate limiting:
// src/middleware/rateLimit.ts
export class RateLimiter {
private requests = new Map<string, number[]>();
isAllowed(clientId: string, maxRequests: number, windowMs: number): boolean {
const now = Date.now();
const clientRequests = this.requests.get(clientId) || [];
// Remove old requests outside the window
const validRequests = clientRequests.filter(time => now - time < windowMs);
if (validRequests.length >= maxRequests) {
return false;
}
validRequests.push(now);
this.requests.set(clientId, validRequests);
return true;
}
}
Secure Storage
Implement secure storage for sensitive data:
// src/utils/encryption.ts
import crypto from 'crypto';
export class SecureStorage {
private static readonly ALGORITHM = 'aes-256-cbc';
static encrypt(text: string, password: string): string {
const key = crypto.scryptSync(password, 'salt', 32);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipher(this.ALGORITHM, key);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
static decrypt(encryptedText: string, password: string): string {
const [ivHex, encrypted] = encryptedText.split(':');
const key = crypto.scryptSync(password, 'salt', 32);
const iv = Buffer.from(ivHex, 'hex');
const decipher = crypto.createDecipher(this.ALGORITHM, key);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
Contributing
Code Style
Follow the established code style:
// Use TypeScript strict mode
// Prefer async/await over promises
// Use meaningful variable names
// Add JSDoc comments for public APIs
/**
* Gets the balance of an address
* @param address - The Ethereum address to check
* @returns Promise resolving to balance in wei
*/
export async function getBalance(address: string): Promise<string> {
// Implementation
}
Pull Request Process
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
Documentation
Update documentation for new features:
- Add tool documentation to the appropriate files
- Update API references
- Include usage examples
- Update integration guides if needed
Deployment
Production Build
# Clean previous build
npm run clean
# Build for production
npm run build
# Verify build
node dist/index.js --version
Docker Deployment
Create a Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/
EXPOSE 3000
CMD ["node", "dist/index.js"]
Build and run:
docker build -t mcp-0g-server .
docker run -p 3000:3000 -e ZEROQ_RPC_URL=https://evmrpc-testnet.0g.ai mcp-0g-server
Monitoring in Production
Set up monitoring and alerting:
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage()
});
});
This development guide provides the foundation for extending and contributing to the MCP 0G Server. For specific questions or advanced use cases, refer to the community resources and documentation.