Part 1 of the Journey: The Chaos Era - The Problem & The Pain Next: The TypeScript Migration Journey
From Monolithic to Modular: The 5,847-Line index.ts Nightmare
How we refactored 17 massive single-file MCP servers into organized, maintainable architectures
Historical Context (November 2025): Built July-August 2025, during the first 60 days of the MCP packaging ecosystem. Manual server development was standard practice—we were discovering architectural patterns while the tooling was being invented. This documents real problems we encountered before best practices existed.
Date: September 25, 2025 Author: Myron Koch & Claude Code Category: Refactoring War Stories
The Horror
Opening ethereum-mcp-server/index.js for the first time:
// Line 1
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
// Line 187
function validateAddress(address) {
// ... validation logic
}
// Line 423
async function getBalance(args) {
// ... balance logic
}
// Line 687
const NETWORK_CONFIG = {
// ... 200 lines of config
}
// Line 1,245
async function deployContract(bytecode, abi) {
// ... deployment logic
}
// Line 2,890
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// THE SWITCH STATEMENT FROM HELL
switch(name) {
case 'eth_get_balance':
// ... 50 lines
case 'eth_send_transaction':
// ... 120 lines
case 'eth_deploy_contract':
// ... 200 lines
// ... 43 more cases
}
});
// Line 5,847
startServer();
One file. 5,847 lines. 47 tools. Zero organization.
The Archaeology
First, we had to understand what was even IN these files:
// scripts/analyze-monolith.js
const content = fs.readFileSync('index.js', 'utf8');
const lines = content.split('\n');
let stats = {
imports: 0,
functions: 0,
configs: 0,
tools: 0,
helpers: 0,
unknown: 0
};
// Archaeological dig through the code
lines.forEach((line, index) => {
if (line.includes('import')) stats.imports++;
else if (line.includes('async function')) stats.functions++;
else if (line.includes('case \'')) stats.tools++;
else if (line.includes('const') && index < 1000) stats.configs++;
// ... more pattern detection
});
console.log(`
Found in ethereum-mcp-server/index.js:
- ${stats.imports} imports (scattered throughout)
- ${stats.functions} functions (mixed purposes)
- ${stats.tools} tool handlers (in switch statement)
- ${stats.configs} config constants (why?)
- ${stats.unknown} lines of mystery code
`);
The Extraction Surgery
Phase 1: Identify Tool Boundaries
We built a parser to find tool implementations:
// scripts/extract-tools.js
function extractTools(monolithPath) {
const content = fs.readFileSync(monolithPath, 'utf8');
const tools = [];
// Find the giant switch statement
const switchMatch = content.match(/switch\s*\(name\)\s*{([\s\S]*?)^}/m);
const switchContent = switchMatch[1];
// Extract each case
const caseRegex = /case\s+['"]([^'"']+)['"]:[
\s\S]*?(?=case\s+['"]|default:|$)/g;
let match;
while ((match = caseRegex.exec(switchContent)) !== null) {
const toolName = match[1];
const toolCode = extractUntilBreak(match[0]);
tools.push({ name: toolName, code: toolCode });
}
return tools;
}
Phase 2: Trace Dependencies
Each tool touched random functions scattered throughout:
function traceDependencies(toolCode, monolithContent) {
const dependencies = new Set();
// Find all function calls
const calls = toolCode.match(/\b(\w+)\s*\(/g) || [];
calls.forEach(call => {
const funcName = call.replace('(', '').trim();
// Find function definition
const funcRegex = new RegExp(`function\s+${funcName}\s*\([^)]*\)\s*{([^}]+)}`, 'g');
const funcMatch = funcRegex.exec(monolithContent);
if (funcMatch) {
dependencies.add({
name: funcName,
code: funcMatch[0],
line: getLineNumber(monolithContent, funcMatch.index)
});
}
});
return dependencies;
}
Phase 3: Generate Modular Files
The actual extraction:
function generateModularTool(tool, dependencies, prefix) {
const fileName = `${prefix}-${camelToKebab(tool.name.replace(prefix + '_', ''))}.ts`;
const content = `
import { BlockchainClient } from '../../client.js';
${generateImports(dependencies)}
export async function handle${capitalizeFirst(tool.name)}(
args: any,
client: BlockchainClient
): Promise<{ content: Array<{ type: string; text: string }> }> {
${cleanupToolCode(tool.code)}
}
${dependencies.map(dep => dep.code).join('\n\n')}
`;
const outputPath = `src/tools/core/${fileName}`;
fs.writeFileSync(outputPath, content);
console.log(`Created: ${outputPath}`);
}
The Untangling
The hardest part? Shared state:
// OLD: Global state in monolith
let web3Instance;
let currentNetwork = 'mainnet';
let gasPrice = null;
const tokenCache = {};
// NEW: Client encapsulation
export class EthereumClient {
private web3: Web3;
private network: string;
private cache: Map<string, any>;
constructor(network: string = 'mainnet') {
this.network = network;
this.web3 = new Web3(NETWORKS[network].rpcUrl);
this.cache = new Map();
}
}
The Config Extraction
Configurations were scattered like landmines:
// Found these gems throughout the monolith:
// Line 234
const MAINNET_RPC = 'https://mainnet.infura.io/v3/';
// Line 1,567
const GAS_LIMIT = 21000;
// Line 3,234
const NETWORKS = { /* finally, some organization */ };
// Line 4,890
const DEFAULT_SLIPPAGE = 0.5;
Consolidated into:
// src/config/network.ts
export const NETWORKS = {
mainnet: {
rpcUrl: process.env.ETH_MAINNET_RPC || 'https://mainnet.infura.io/v3/',
chainId: 1,
explorer: 'https://etherscan.io',
gasLimit: 21000,
defaultSlippage: 0.5
},
// ... all configs in ONE place
};
The Index.ts Shrinking
The beautiful result:
// NEW src/index.ts - Just 287 lines!
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { EthereumClient } from './client.js';
// Import all tool handlers
import { handleEthGetBalance } from './tools/core/eth-get-balance.js';
import { handleEthGetTransaction } from './tools/core/eth-get-transaction.js';
// ... more imports
// Clean tool registry
const toolHandlers: Record<string, Function> = {
'eth_get_balance': handleEthGetBalance,
'eth_get_transaction': handleEthGetTransaction,
// ... just registration, no implementation
};
// Simple request handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (toolHandlers[name]) {
return await toolHandlers[name](args, client);
}
throw new Error(`Unknown tool: ${name}`);
});
The Numbers
Ethereum MCP Server Transformation:
Before:
- 1 file
- 5,847 lines
- 0 tests
- 47 inline tool implementations
- ∞ cognitive load
After:
- 58 files
- ~100 lines each
- 47 tool files
- 3 test suites
- Comprehensible by humans
The Patterns We Found
Monolithic servers all had the same anti-patterns:
1. The Kitchen Sink Import Section
// 50+ imports at the top
import Web3 from 'web3';
import { ethers } from 'ethers';
import axios from 'axios';
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
// ... why is crypto here? who knows!
2. The Random Helper Function Graveyard
// Scattered throughout like breadcrumbs
function sleep(ms) { /* ... */ } // Line 234
function retry(fn) { /* ... */ } // Line 1,890
function sleep2(ms) { /* ... */ } // Line 3,456 (yes, sleep2!)
function retryWithBackoff(fn) { /* ... */ } // Line 4,234
3. The Configuration Archaeology Layers
// Different eras of development visible
const RPC_URL = 'old.rpc.url'; // 2023
const RPC_ENDPOINT = 'newer.rpc'; // 2024
const NETWORK_CONFIG = { rpc: 'newest.rpc' }; // 2025
// All three still referenced somewhere!
The Automated Refactoring Script
We built the ultimate refactoring weapon:
#!/usr/bin/env node
// scripts/refactor-monolith.js
async function refactorMonolithicServer(serverPath) {
console.log(`🔨 Refactoring ${serverPath}`);
// 1. Backup original
execSync(`cp ${serverPath}/index.js ${serverPath}/index.js.backup`);
// 2. Extract tools
const tools = extractTools(`${serverPath}/index.js`);
console.log(`📦 Extracted ${tools.length} tools`);
// 3. Generate modular structure
createDirectoryStructure(serverPath);
// 4. Generate tool files
tools.forEach(tool => {
generateToolFile(tool, serverPath);
});
// 5. Generate new index.ts
generateCleanIndex(tools, serverPath);
// 6. Generate client.ts
generateClientAbstraction(serverPath);
// 7. Generate tests
generateTestSuites(tools, serverPath);
// 8. Update package.json
updatePackageJson(serverPath);
console.log(`✅ Refactored successfully!`);
console.log(` Before: 1 file, ${getLineCount(`${serverPath}/index.js.backup`)} lines`);
console.log(` After: ${countFiles(serverPath)} files, modular structure`);
}
// Run on all servers
const servers = findMonolithicServers();
servers.forEach(server => refactorMonolithicServer(server));
The Lessons Learned
1. Start Modular, Stay Sane
Every new server now starts with the modular structure. No exceptions.
2. One Tool, One File, One Purpose
Never again will we have a tool implementation scattered across 500 lines.
3. The Client Pattern is Sacred
All blockchain interaction through a client class. Always.
4. Tests are Part of Refactoring
No refactor is complete without tests. The tests often reveal hidden bugs.
5. Keep the Old File
We kept .backup files. Saved us multiple times when we needed to check original logic.
The Unexpected Discovery
During refactoring, we found:
- Duplicate functions: 3 different
getBalanceimplementations in one file - Dead code: ~30% was never called
- Hidden bugs: Race conditions masked by synchronous appearance
- Copy-paste errors: Tool returning wrong data structure
- Version conflicts: Using 3 different Web3 versions’ APIs
The Final Structure
Every server now follows this pattern:
src/
├── index.ts (< 300 lines - just wiring)
├── client.ts (blockchain interaction)
├── config/
│ └── network.ts (all configuration)
├── tools/
│ ├── core/ (25 required tools)
│ ├── wallet/ (5-10 wallet tools)
│ ├── tokens/ (10-15 token tools)
│ ├── nft/ (if supported)
│ ├── defi/ (if applicable)
│ └── [domain]/ (chain-specific)
├── types/
│ └── index.ts (TypeScript definitions)
└── utils/
├── logger.ts (never console.log)
├── validation.ts (input validation)
└── format.ts (output formatting)
The Refactoring Checklist
For anyone facing their own monolith:
- Backup everything first
- Run extraction analysis
- Map tool boundaries
- Identify shared dependencies
- Extract configuration
- Generate modular structure
- Create client abstraction
- Build tool files
- Wire new index.ts
- Add comprehensive tests
- Validate with MCP inspector
- Delete monolith (after 1 week safety period)
The Victory
17 monolithic nightmares transformed into organized, maintainable, testable architectures.
Total lines refactored: 67,234 Total files created: 1,247 Sanity preserved: Priceless
References
- Refactoring script:
/scripts/refactor-monolith.js - Extraction tools:
/scripts/extract-tools.js - Before examples:
/archives/monolithic-horrors/ - After examples: All current servers in
/servers/
This is part of our ongoing series documenting architectural patterns and insights from building the Blockchain MCP Server Ecosystem. Sometimes you have to burn it down to build it right.
Related Reading
Prerequisites
- This is the first post in the series.
Next Steps
- The TypeScript Migration Journey - After seeing the chaos, understand the first step we took to fix it.
Deep Dives
- The MBPS v2.1 Standard: How Chaos Became Order - See the standard that this refactoring helped enable.
- From Manual to Meta: The Complete MCP Factory Story - Understand how this modular structure was critical for building the automation factory.