Copy-Paste Architecture: When Duplication Beats Abstraction
Part 1 of the Journey: The Chaos Era - The Problem & The Pain Previous: The Git Submodule Disaster | Next: ESM/CommonJS Migration Hell
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 block
chain 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.jsServer 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.

