back to posts
#04 Part 1 2025-07-15 10 min

Copy-Paste Architecture: When Duplication Beats Abstraction

How we learned to stop worrying and embrace strategic code duplication across 17 blockchain servers

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:

3. The Context Window Problem

We work with AI assistants. Every abstraction layer adds context:

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:

No coordination needed.

2. Faster Onboarding

New developer? Pick ONE server, understand it completely:

3. AI-Friendly Development

Claude or GPT can see the entire server in one context:

4. Failure Isolation

Break Solana? Other 16 servers still work:

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:

  1. Start with duplication - Copy first, abstract later (maybe)
  2. Earn your abstractions - Need it 3+ times? Still might not abstract
  3. Prefer scripts over frameworks - Batch updates > automatic coupling
  4. Keep servers independent - Each should build/test/deploy alone
  5. 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:

Our response:

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


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.


Prerequisites

Next Steps

Deep Dives