Part 1 of the Journey: The Chaos Era - The Problem & The Pain Previous: The Git Submodule Disaster | Next: ESM/CommonJS Migration Hell
Copy-Paste Architecture: When Duplication Beats Abstraction
How we learned to stop worrying and embrace strategic code duplication across 17 blockchain servers
Historical Context (November 2025): Built July 2025, during early MCP development. Strategic duplication became a key pattern that later informed our Factory automation—sometimes repetition teaches patterns better than abstraction.
Date: July 15, 2025 Author: Myron Koch & Claude Code Category: Architecture Philosophy
The Orthodox Sin
Every programming course teaches: “Don’t Repeat Yourself” (DRY). Duplication is evil. Abstract everything. Create reusable components.
We started with this mindset. 17 blockchain servers? Obviously we needed a shared library, right?
Wrong.
The Failed Abstraction Attempt
January 2025. We tried creating @blockchain-mcp/core:
// The "perfect" abstraction
export abstract class BlockchainMCPServer {
abstract getBalance(address: string): Promise<Balance>;
abstract getTransaction(hash: string): Promise<Transaction>;
abstract sendTransaction(tx: TransactionRequest): Promise<string>;
// ... 50 more abstract methods
}
export class EthereumMCPServer extends BlockchainMCPServer {
// Implement everything for Ethereum
}
export class SolanaMCPServer extends BlockchainMCPServer {
// Force Solana into Ethereum's shape
}
The result? A disaster.
Why Abstraction Failed
1. Blockchains Are Fundamentally Different
// Ethereum: Account-based
const balance = await provider.getBalance(address);
// Bitcoin: UTXO-based
const utxos = await provider.getUTXOs(address);
const balance = utxos.reduce((sum, utxo) => sum + utxo.value, 0);
// NEAR: Named accounts
const account = await near.account("alice.near");
const balance = await account.getAccountBalance();
// Sui: Object-based
const objects = await provider.getOwnedObjects(address);
const balance = calculateFromObjects(objects);
Forcing these into one interface? Madness.
2. The Dependency Hell
Shared library meant:
- All 17 servers updated when we changed anything
- Version conflicts between blockchain SDKs
- Build failures cascading across servers
- Testing nightmares with different node versions
3. The Context Window Problem
We work with AI assistants. Every abstraction layer adds context:
- Base classes to understand
- Inheritance chains to follow
- Abstract methods to implement
- Type hierarchies to navigate
Our context windows exploded.
The Copy-Paste Revelation
February 2025. We gave up on abstraction and embraced duplication:
# Create new server? Copy a working one
cp -r ethereum-sepolia-mcp-server solana-devnet-mcp-server
# Change the blockchain-specific parts
# Keep the MCP boilerplate
And it worked beautifully.
The Pattern That Emerged
Each server is self-contained:
polygon-mcp-server/
├── src/
│ ├── index.ts # ~300 lines, mostly the same
│ ├── client.ts # Blockchain-specific
│ └── tools/ # Mix of copied and custom
├── package.json # Independent dependencies
└── tsconfig.json # Independent config
90% of index.ts is identical across servers. And that’s FINE.
Strategic Duplication Points
1. MCP Boilerplate (Always Copied)
// This is in EVERY server
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: Object.entries(tools).map(([name, config]) => ({
name,
description: config.description,
inputSchema: config.inputSchema,
})),
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// ... handle tool call
});
2. Tool Patterns (Often Copied, Then Modified)
// get_balance is similar everywhere
export async function handleGetBalance(args: any, client: BlockchainClient) {
const { address } = args;
// Validate address (copied)
if (!isValidAddress(address)) {
throw new Error('Invalid address');
}
// Get balance (blockchain-specific)
const balance = await client.getBalance(address);
// Format response (copied)
return {
content: [{
type: 'text',
text: JSON.stringify({ address, balance }, null, 2)
}]
};
}
3. Configuration (Copied Structure, Different Values)
// Every server has this structure
export const NETWORKS = {
mainnet: {
rpcUrl: 'https://different-for-each-chain',
chainId: 1, // Different value
explorer: 'https://different-explorer'
},
testnet: {
rpcUrl: 'https://different-testnet',
chainId: 5,
explorer: 'https://different-testnet-explorer'
}
};
The Benefits We Didn’t Expect
1. Independent Evolution
Each server can evolve at its own pace:
- Osmosis has 158 tools
- Bitcoin has 87 tools
- Ethereum has 50 tools
No coordination needed.
2. Faster Onboarding
New developer? Pick ONE server, understand it completely:
- No inheritance to trace
- No abstractions to decode
- No “magic” from base classes
- Just straightforward code
3. AI-Friendly Development
Claude or GPT can see the entire server in one context:
- No need to fetch parent classes
- No need to understand frameworks
- Complete implementation visible
- Direct modifications possible
4. Failure Isolation
Break Solana? Other 16 servers still work:
- No shared dependencies
- No cascading failures
- No version conflicts
- Independent deployment
The Script That Makes It Work
When patterns need updating across servers:
// scripts/update-pattern.js
const servers = glob.sync('servers/*/ *-mcp-server');
servers.forEach(server => {
const indexPath = `${server}/src/index.ts`;
let content = fs.readFileSync(indexPath, 'utf8');
// Update MCP boilerplate
content = content.replace(
/server\.setRequestHandler\(ListToolsRequestSchema[\s\S]*?\}\);/,
NEW_BOILERPLATE
);
fs.writeFileSync(indexPath, content);
console.log(`Updated ${server}`);
});
Selective, intentional propagation. Not automatic coupling.
When NOT to Copy-Paste
We still abstract when it makes sense:
1. Logger Module (Shared via npm)
// This is published as @blockchain-mcp/logger
import { createLogger } from '@blockchain-mcp/logger';
const logger = createLogger('ethereum-mcp');
2. Testing Utilities (Shared via npm)
// @blockchain-mcp/test-utils
import { mockMCPServer, validateToolNaming } from '@blockchain-mcp/test-utils';
3. Type Definitions (Sometimes Shared)
// When types truly align across chains
import { MCPResponse, ToolDefinition } from '@blockchain-mcp/types';
The Philosophy
Copy-paste is a tool, not a failure.
Rules we follow:
- Start with duplication - Copy first, abstract later (maybe)
- Earn your abstractions - Need it 3+ times? Still might not abstract
- Prefer scripts over frameworks - Batch updates > automatic coupling
- Keep servers independent - Each should build/test/deploy alone
- Document the patterns - Scripts show what should be consistent
Real-World Example: The Winston Logger Propagation
We needed to replace console.log across all servers:
// Did we create LoggerBase class? No.
// Did we make @blockchain-mcp/logger-framework? No.
// We created ONE logger.ts file
// Then literally copied it 17 times
cp ethereum-mcp-server/src/utils/logger.ts solana-mcp-server/src/utils/logger.ts
cp ethereum-mcp-server/src/utils/logger.ts bitcoin-mcp-server/src/utils/logger.ts
// ... 14 more times
// Then ran the console.log purge script on each
Time to implement: 1 hour Time if we’d built an abstraction: 1 week
The Judgment
Senior developers judge our codebase:
- “So much duplication!”
- “This violates DRY!”
- “You need a framework!”
Our response:
- 17 working servers
- Independent deployment
- New server in 30 minutes
- AI can modify any server instantly
- Zero coupling between servers
The Lesson
In distributed systems, coupling is more dangerous than duplication.
In AI-assisted development, clarity beats cleverness.
In rapid prototyping, working beats perfect.
Sometimes, the best architecture is 17 copies of a good pattern, independently evolving, strategically synchronized when needed.
Embrace the copy-paste.
Code References
- Pattern scripts:
/scripts/update-pattern.js - Server templates:
/servers/templates/ - Original abstraction attempt:
/archives/failed-abstractions/blockchain-mcp-core/ - Current servers:
/servers/testnet/and/servers/mainnet/
This is part of our ongoing series documenting architectural patterns and insights from building the Blockchain MCP Server Ecosystem. Sometimes the best practice isn’t best practice.
Related Reading
Prerequisites
- The Git Submodule Disaster - Understand the problem that led to this architectural choice.
Next Steps
- ESM/CommonJS Migration Hell - See another area where pragmatic choices were required over theoretical purity.
Deep Dives
- The MBPS v2.1 Standard: How Chaos Became Order - Learn how we enforced consistency across duplicated codebases.
- From Manual to Meta: The Complete MCP Factory Story - The ultimate expression of this philosophy: a factory that automates the copy-paste process.