back to posts
#05 Part 1 2025-10-07 18 min

ESM/CommonJS Migration Hell: When Bitcoin Testnet Broke Everything

The definitive guide to debugging MCP module compatibility issues

Part 1 of the Journey: The Chaos Era - The Problem & The Pain Previous: Copy-Paste Architecture | Next: The Logger Revolution

ESM/CommonJS Migration Hell: When Bitcoin Testnet Broke Everything

The definitive guide to debugging MCP module compatibility issues

Historical Context (November 2025): Crisis occurred September 16, 2025, during manual server development phase. We were navigating Node.js module system complexities before MCP-specific solutions emerged. This debugging story remains relevant for understanding module compatibility in MCP servers.

Date: October 7, 2025 (Story from September 16, 2025) Author: Myron Koch & Claude Desktop Category: MCP Debugging, TypeScript, Node.js Modules Reading Time: 18 minutes


The Crisis at 13/14

You know that feeling when you build something beautiful, everything’s working perfectly, and then you add one more piece and the entire thing collapses?

September 16, 2025. I’m reviewing my blockchain MCP ecosystem. Fourteen servers total. Thirteen of them responding beautifully to Claude Desktop:

✅ Ethereum Sepolia - 9.2M blocks indexed
✅ Polygon Mainnet - 76.5M blocks, 53.5 TPS
✅ Arbitrum Mainnet - 379M+ blocks
✅ Avalanche C-Chain - 68.8M blocks
✅ Cosmos Hub - 27.5M blocks
✅ BNB Chain - 61.3M blocks
✅ Solana Testnet - 358M slots
✅ XRP Ledger
✅ NEAR Protocol
✅ Osmosis
✅ Sui Network
✅ Base Sepolia
✅ Cloudflare Workers

And then there’s Bitcoin testnet:

Won’t load
Claude Desktop logs full of errors
Every tool invocation fails
Complete radio silence

Thirteen working servers. One completely dead. All using the same architecture, same patterns, same copy-paste structure.

What makes one server different?

This is the story of how three missing configuration lines can bring down an entire blockchain server, and why “ESM/CommonJS compatibility” is the phrase that should strike fear into every Node.js developer’s heart.


Act I: The Error Messages

The First Clue

I check Claude Desktop’s logs (~/Library/Logs/Claude/mcp*.log):

[bitcoin-testnet] ReferenceError: exports is not defined in ES module scope
[bitcoin-testnet] This file is being treated as an ES module because it has a .js extension
[bitcoin-testnet] and package.json contains "type": "module"
[bitcoin-testnet] To treat it as a CommonJS script, rename it to use the .cjs extension

Wait. “exports is not defined”? But I’m not using exports anywhere. I’m writing TypeScript with proper ESM imports:

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

export const server = new Server(/* ... */);

How is exports even in the picture?

The Second Error

I try to start the server manually:

$ node dist/index.js
Error [ERR_REQUIRE_ESM]: require() of ES Module /node_modules/@modelcontextprotocol/sdk/server/index.js not supported.
Instead change the require of index.js in /dist/index.js to a dynamic import() which is available in all CommonJS modules.

Now it’s telling me I’m using require() on an ES module. But I’m not using require() at all!

$ grep -r "require(" dist/
dist/index.js:const sdk_1 = require("@modelcontextprotocol/sdk/server/index.js");

There it is.

My TypeScript source has import statements, but my compiled JavaScript output has require() calls.

TypeScript is betraying me.

The Third Error

For completeness, I check the MCP SDK:

$ npm list @modelcontextprotocol/sdk
bitcoin-testnet-mcp-server@1.0.0
└── @modelcontextprotocol/sdk@0.5.0

Version 0.5.0.

I check one of the working servers:

$ cd ../ethereum-sepolia-mcp-server
$ npm list @modelcontextprotocol/sdk
ethereum-sepolia-mcp-server@1.0.0
└── @modelcontextprotocol/sdk@1.0.3

Version 1.0.3.

Three problems. Three different layers. All connected.


Act II: The Investigation

Comparing Configurations

I pull up package.json from a working server (Ethereum Sepolia) and the broken server (Bitcoin testnet) side by side.

Ethereum (Working):

{
  "name": "ethereum-sepolia-mcp-server",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.js",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.3"
  }
}

Bitcoin (Broken):

{
  "name": "bitcoin-testnet-mcp-server",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.5.0"
  }
}

Missing line: "type": "module"

The TypeScript Configuration

Next, tsconfig.json:

Ethereum (Working):

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

Bitcoin (Broken):

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

Wrong setting: "module": "commonjs" instead of "ESNext"

The Pattern

Every working server has:

  1. "type": "module" in package.json
  2. "module": "ESNext" in tsconfig.json
  3. ✅ SDK version ^1.0.0 or higher

Bitcoin testnet has:

  1. ❌ No "type": "module"
  2. "module": "commonjs"
  3. ❌ SDK version 0.5.0

Three missing pieces. Complete failure.


Act III: Understanding the Enemy

Before we fix this, we need to understand what we’re fighting. This isn’t just a configuration error - it’s a fundamental clash between two module systems that were never meant to coexist.

A Brief History of JavaScript Modules

In the Beginning: No Modules

Early JavaScript (1995-2009):

// Everything in global scope
var myFunction = function() { /* ... */ };
var myData = { /* ... */ };

Every variable was global. Every script could see every other script’s variables. It was chaos.

Then: CommonJS (2009)

Node.js introduced CommonJS modules:

// module.js
const something = require('./other.js');
module.exports = { myFunction, myData };

// Using it
const myModule = require('./module.js');

Synchronous loading. Perfect for servers, terrible for browsers.

The Standard: ECMAScript Modules (2015)

ES6 introduced official JavaScript modules:

// module.js
import { something } from './other.js';
export { myFunction, myData };

// Using it
import { myFunction } from './module.js';

Asynchronous loading. Works everywhere. The future.

The Problem: Compatibility

Node.js has to support both. Forever. Because billions of lines of CommonJS code exist.

So we get this nightmare scenario:

The Three-Part Configuration

For MCP servers to work, three things must agree:

1. package.json: Tell Node.js you’re using ESM

{
  "type": "module"
}

Without this, Node.js assumes CommonJS (for backward compatibility).

2. tsconfig.json: Tell TypeScript to output ESM

{
  "compilerOptions": {
    "module": "ESNext"
  }
}

Without this, TypeScript outputs require()/module.exports (CommonJS).

3. MCP SDK: Use version that supports pure ESM

{
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0"
  }
}

MCP SDK 1.0.0+ is pure ESM. Version 0.5.0 tried to support both (and failed).

What Happens When They Disagree

Scenario 1: Missing “type: module”

Node.js assumes CommonJS. TypeScript outputs ESM. Node tries to load ESM as CommonJS:

Error: Cannot use import statement outside a module

Scenario 2: TypeScript outputs CommonJS, SDK is ESM

TypeScript generates require() calls. SDK only exports ESM:

Error [ERR_REQUIRE_ESM]: require() of ES Module not supported

Scenario 3: Package says ESM, TypeScript outputs CommonJS

Node expects ESM. TypeScript outputs CommonJS. Node sees exports:

ReferenceError: exports is not defined in ES module scope

This was us. Bitcoin testnet had scenario 3.


Act IV: The Fix

Step 1: Update package.json

cd bitcoin-testnet-mcp-server

Edit package.json:

{
  "name": "bitcoin-testnet-mcp-server",
  "version": "1.0.0",
  "type": "module",  // ← ADD THIS
  "main": "./dist/index.js",
  "bin": {
    "bitcoin-testnet-mcp-server": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc && node -e \"require('fs').chmodSync('./dist/index.js', '755')\"",
    "prepare": "npm run build"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.3",  // ← UPDATE THIS (was 0.5.0)
    "axios": "^1.7.7",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.7.0"
  }
}

Step 2: Update tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",        // ← CHANGE FROM "commonjs"
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Step 3: Rebuild

# Clean old build
rm -rf dist/ node_modules/

# Fresh install
npm install

# Build
npm run build

Step 4: Verify the Output

Check what TypeScript generated:

$ head -20 dist/index.js
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

import statements (not require()) ✅ .js extensions on imports ✅ No exports variable

Perfect.

Step 5: Test

# Try running directly
$ node dist/index.js
# Should start without errors

# Test with MCP Inspector
$ npx @modelcontextprotocol/inspector dist/index.js

# Check in Claude Desktop
# Add to claude_desktop_config.json and restart

The Result

Bitcoin testnet MCP server: fully operational.

50+ tools now available:

From zero to hero with three configuration fixes.


Act V: The Systematic Methodology

Creating the Handoff Document

I immediately created MCP_BLOCKCHAIN_DEBUGGING_HANDOFF.md with the complete fix pattern:

# MCP Blockchain Debugging Handoff

**Date**: September 16, 2025
**Status**: Bitcoin testnet FIXED, 13 servers remaining

## The Fix Pattern

For any broken MCP server with ESM/CommonJS issues:

### 1. Check Configuration
- [ ] package.json has `"type": "module"`
- [ ] tsconfig.json has `"module": "ESNext"`  
- [ ] SDK version is ^1.0.0+

### 2. Apply Fixes
```bash
# Update package.json
npm install @modelcontextprotocol/sdk@latest

# Add "type": "module" to package.json

# Update tsconfig.json module to "ESNext"

# Rebuild
rm -rf dist/ node_modules/
npm install
npm run build

3. Verify


### The Automated Checker

I created a script to detect ESM compatibility issues:

```bash
#!/bin/bash
# esm-compatibility-check.sh
# Validates MCP server ESM configuration

SERVER_PATH="${1:-.}"
ERRORS=0

echo "╔═══════════════════════════════════════════════════╗"
echo "║   MCP Server ESM Compatibility Checker v1.0      ║"
echo "╚═══════════════════════════════════════════════════╝"
echo ""
echo "Checking: $SERVER_PATH"
echo ""

# Check 1: package.json type
echo "━━━ 1. Checking package.json 'type' field..."
if grep -q '"type": *"module"' "$SERVER_PATH/package.json" 2>/dev/null;
then
    echo "✅ ESM mode enabled (\"type\": \"module\")"
else
    echo "❌ MISSING: \"type\": \"module\" in package.json"
    echo "   → Add: \"type\": \"module\" to package.json"
    ERRORS=$((ERRORS + 1))
fi
echo ""

# Check 2: MCP SDK version
echo "━━━ 2. Checking MCP SDK version..."
SDK_VERSION=$(grep -o '@modelcontextprotocol/sdk": *"[^"*"' "$SERVER_PATH/package.json" 2>/dev/null | grep -o '[0-9][^"*'])
if [ -z "$SDK_VERSION" ]; then
    echo "❌ MCP SDK not found in dependencies"
    ERRORS=$((ERRORS + 1))
elif [[ "$SDK_VERSION" =~ ^1\. ]] || [[ "$SDK_VERSION" =~ ^\^ ]]; then
    echo "✅ SDK version $SDK_VERSION (compatible)"
else
    echo "❌ SDK version $SDK_VERSION (requires 1.0.0+)"
    echo "   → Update: npm install @modelcontextprotocol/sdk@latest"
    ERRORS=$((ERRORS + 1))
fi
echo ""

# Check 3: tsconfig.json module
echo "━━━ 3. Checking tsconfig.json 'module' setting..."
if grep -q '"module": *"ESNext"' "$SERVER_PATH/tsconfig.json" 2>/dev/null;
then
    echo "✅ TypeScript module: ESNext"
elif grep -q '"module": *"ES2022"' "$SERVER_PATH/tsconfig.json" 2>/dev/null;
then
    echo "✅ TypeScript module: ES2022 (compatible)"
elif grep -q '"module": *"ES2020"' "$SERVER_PATH/tsconfig.json" 2>/dev/null;
then
    echo "⚠️  TypeScript module: ES2020 (may work, but ESNext recommended)"
else
    MODULE_TYPE=$(grep -o '"module": *"[^"*"' "$SERVER_PATH/tsconfig.json" 2>/dev/null)
    echo "❌ TypeScript module: $MODULE_TYPE (incompatible)"
    echo "   → Change to: \"module\": \"ESNext\""
    ERRORS=$((ERRORS + 1))
fi
echo ""

# Check 4: Build output
echo "━━━ 4. Checking build output..."
if [ ! -f "$SERVER_PATH/dist/index.js" ]; then
    echo "⚠️  No build output found"
    echo "   → Run: npm run build"
else
    # Check for require() in compiled output
    if grep -q 'require(' "$SERVER_PATH/dist/index.js" 2>/dev/null;
then
        echo "❌ Build output contains require() calls"
        echo "   → TypeScript is generating CommonJS instead of ESM"
        echo "   → Verify tsconfig.json and rebuild"
        ERRORS=$((ERRORS + 1))
    else
        echo "✅ Build output uses ESM imports"
    fi
    
    # Check for proper shebang
    if head -n 1 "$SERVER_PATH/dist/index.js" | grep -q '#!/usr/bin/env node'; then
        echo "✅ Shebang present in output"
    else
        echo "⚠️  Missing shebang in dist/index.js"
        echo "   → Add to src/index.ts: #!/usr/bin/env node"
    fi
fi
echo ""

# Check 5: Source code patterns
echo "━━━ 5. Checking source code for CommonJS patterns..."
if grep -r 'require(' "$SERVER_PATH/src/" 2>/dev/null | grep -v "node_modules" | grep -v ".spec." | grep -v ".test."; then
    echo "❌ Found require() in source code"
    echo "   → Convert to: import { X } from 'Y'"
    ERRORS=$((ERRORS + 1))
else
    echo "✅ No require() found in source"
fi

if grep -r 'module\.exports' "$SERVER_PATH/src/" 2>/dev/null | grep -v "node_modules"; then
    echo "❌ Found module.exports in source code"
    echo "   → Convert to: export { X }"
    ERRORS=$((ERRORS + 1))
else
    echo "✅ No module.exports found in source"
fi
echo ""

# Summary
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [ $ERRORS -eq 0 ]; then
    echo "✅ ALL CHECKS PASSED"
    echo ""
    echo "This MCP server is properly configured for ESM."
    exit 0
else
    echo "❌ FOUND $ERRORS ISSUE(S)"
    echo ""
    echo "Fix the issues above and re-run this checker."
    exit 1
fi

Make it executable:

chmod +x esm-compatibility-check.sh

Use it on any MCP server:

./esm-compatibility-check.sh /path/to/mcp-server

The Minimal Working Template

For Future MCP Servers

Here’s the exact configuration that works for every blockchain MCP server:

package.json:

{
  "name": "your-mcp-server",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.js",
  "bin": {
    "your-mcp-server": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc && node -e \"require('fs').chmodSync('./dist/index.js', '755')\"",
    "prepare": "npm run build",
    "watch": "tsc --watch"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.3",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.7.0"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

src/index.ts:

#!/usr/bin/env node

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';

// Define your tools
const YOUR_TOOL_SCHEMA = z.object({
  param: z.string().describe('Parameter description'),
});

// Create server
const server = new Server(
  {
    name: 'your-mcp-server',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: 'your_tool',
        description: 'Description of what your tool does',
        inputSchema: {
          type: 'object',
          properties: {
            param: {
              type: 'string',
              description: 'Parameter description',
            },
          },
          required: ['param'],
        },
      },
    ],
  };
});

// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    if (name === 'your_tool') {
      const parsed = YOUR_TOOL_SCHEMA.parse(args);
      
      // Your tool logic here
      const result = { message: 'Success' };

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(result, null, 2),
          },
        ],
      };
    }

    throw new Error(`Unknown tool: ${name}`);
  } catch (error) {
    if (error instanceof z.ZodError) {
      throw new Error(
        `Invalid arguments: ${error.errors
          .map((e) => `${e.path.join('.')}: ${e.message}`)
          .join(', ')}`
      );
    }
    throw error;
  }
});

// Start server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('MCP server running on stdio');
}

main().catch((error) => {
  console.error('Fatal error:', error);
  process.exit(1);
});

Key points:


Deep Dive: ESM vs CommonJS

The Technical Details

ESM Loading Process:

  1. Parse: Analyze module structure, find imports/exports
  2. Download: Fetch all dependencies (can be async)
  3. Link: Connect imports to exports across modules
  4. Evaluate: Run module code top-to-bottom

CommonJS Loading Process:

  1. Execute: Run module code immediately
  2. Cache: Store exports in module cache
  3. Return: Hand exports back to requiring module

Key Differences:

FeatureESMCommonJS
Syntaximport/exportrequire()/module.exports
LoadingAsynchronousSynchronous
StaticYes (analyzed at parse time)No (dynamic at runtime)
Top-level await✅ Supported❌ Not supported
Tree shaking✅ Excellent⚠️ Limited
Conditional imports⚠️ Complex✅ Easy
File extensionsRequiredOptional

Why MCP SDK Went Pure ESM

MCP SDK 0.5.0:

MCP SDK 1.0.0:

The trade-off:

TypeScript’s Role

TypeScript is module-agnostic. It doesn’t care whether you use ESM or CommonJS. It just compiles your code to whatever you specify:

// Your TypeScript source (same either way)
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
export const server = new Server(/*...*/);

With “module”: “commonjs”:

// Compiled output
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const server_1 = require("@modelcontextprotocol/sdk/server/index.js");
exports.server = new server_1.Server(/*...*/);

With “module”: “ESNext”:

// Compiled output
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
export const server = new Server(/*...*/);

If package.json says ESM but TypeScript outputs CommonJS:

Node.js sees:

That was our bug.


Common Pitfalls & Solutions

Pitfall 1: Forgetting .js Extensions

ESM requires explicit file extensions in imports:

// ❌ This breaks
import { helper } from './utils';

// ✅ This works
import { helper } from './utils.js';

Why: ESM needs to know file type for loading. No extension = Node doesn’t know what to load.

TypeScript quirk: Even though your source is .ts, imports must use .js (TypeScript strips the extension during compilation).

Pitfall 2: Mixed Import Styles

// ❌ Don't mix these
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
const fs = require('fs');  // BREAKS IN ESM

// ✅ Use all imports
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import fs from 'fs';

Pitfall 3: Dynamic Require

// ❌ This CommonJS pattern breaks
const moduleName = 'some-module';
const module = require(moduleName);

// ✅ Use dynamic import (async)
const moduleName = 'some-module';
const module = await import(moduleName);

Pitfall 4: __dirname and __filename

These global variables don’t exist in ESM:

// ❌ Breaks in ESM
console.log(__dirname);

// ✅ ESM equivalent
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

Pitfall 5: JSON Imports

// ❌ Old way (CommonJS)
const package = require('./package.json');

// ✅ ESM way (requires tsconfig setting)
import package from './package.json' assert { type: 'json' };

// Or use fs
import { readFileSync } from 'fs';
const package = JSON.parse(readFileSync('./package.json', 'utf-8'));

Testing Your Configuration

The Complete Checklist

Use this to verify any MCP server:

Configuration:

Build Process:

Runtime:

Integration:

Manual Testing Commands

# 1. Check configuration
grep -E '"type"|"module"' package.json tsconfig.json

# 2. Verify SDK version
npm list @modelcontextprotocol/sdk

# 3. Check build output
head -20 dist/index.js

# 4. Look for CommonJS patterns
grep -E "require\(|module\.exports" dist/index.js

# 5. Test with MCP Inspector
npx @modelcontextprotocol/inspector dist/index.js

# 6. Check Claude Desktop config
cat ~/Library/Application\ Support/Claude/claude_desktop_config.json

# 7. Restart Claude Desktop and check logs
tail -f ~/Library/Logs/Claude/mcp*.log

Lessons Learned

For MCP Development

1. Start with the right template

Don’t copy-paste from old projects. Use the official MCP SDK templates or create your own verified template.

2. Configuration drift is silent

You won’t notice until something breaks. Use automated checkers.

3. Error messages are misleading

“exports is not defined” doesn’t mean you used exports. It means Node.js expected ESM but got CommonJS.

4. Compare working examples

When debugging, find something that works and compare configurations line-by-line.

5. Document your fixes

Future you (or your team) will thank you. Create handoff documents with exact steps.

For TypeScript Projects

1. Module setting is critical

"module": "commonjs" vs "module": "ESNext" completely changes output format.

2. Package.json and tsconfig.json must agree

If package says ESM, TypeScript must output ESM.

3. .js extensions are mandatory in imports

Even in .ts files, even though it feels wrong.

4. Test your build output

Don’t assume TypeScript did what you expected. Check the compiled JavaScript.

For Debugging

1. Look at compiled output first

Source code might be correct, but compilation might be wrong.

2. Check versions systematically

Old SDK versions, old Node.js versions, old TypeScript versions - all can cause issues.

3. Create reproducible test cases

Minimal MCP server that demonstrates the issue.

4. Use automated tooling

Scripts like our ESM checker catch issues before they break production.


The Ecosystem Impact

After fixing Bitcoin testnet, I had a decision: apply this pattern to the remaining 9 blockchain servers immediately, or document first?

I chose documentation.

Why? Because this will happen again:

The MCP_BLOCKCHAIN_DEBUGGING_HANDOFF.md document is now the source of truth:

# Broken MCP Server? Start Here.

1. Run esm-compatibility-check.sh
2. Fix any issues it identifies
3. Rebuild: rm -rf dist/ node_modules/ && npm install && npm run build
4. Test with MCP Inspector
5. Verify in Claude Desktop
6. Mark server as verified in this document

## Verified Servers (14/14)
- [x] claude-memory
- [x] desktop-commander
- [x] filesystem
- [x] ethereum-sepolia
- [x] bitcoin-testnet ← JUST FIXED
- [ ] polygon-mainnet ← NEXT
- [ ] arbitrum-mainnet
- [ ] avalanche-fuji
- [ ] bsc-testnet
- [ ] base-sepolia
- [ ] solana-devnet
- [ ] xrp-testnet
- [ ] near-testnet
- [ ] osmosis-testnet

Systematic. Repeatable. Documentable.


The Fix Template

Quick Reference

For any broken MCP server:

#!/bin/bash
# fix-esm-compatibility.sh

# 1. Update package.json
npm pkg set type=module
npm install @modelcontextprotocol/sdk@latest

# 2. Update tsconfig.json
# (manually edit "module": "ESNext")

# 3. Clean rebuild
rm -rf dist/ node_modules/
npm install
npm run build

# 4. Verify
node dist/index.js &
PID=$!
sleep 2
kill $PID

echo "Fixed! Test with: npx @modelcontextprotocol/inspector dist/index.js"

Before/After Example

Before (Broken):

package.json:

{
  "name": "my-mcp-server",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.5.0"
  }
}

tsconfig.json:

{
  "compilerOptions": {
    "module": "commonjs"
  }
}

Result:

Error [ERR_REQUIRE_ESM]: require() of ES Module not supported

After (Working):

package.json:

{
  "name": "my-mcp-server",
  "type": "module",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.3"
  }
}

tsxconfig.json:

{
  "compilerOptions": {
    "module": "ESNext"
  }
}

Result:

✅ MCP server running on stdio

Conclusion

The most frustrating bugs are configuration mismatches.

Not your code. Not the SDK. Not Node.js being weird.

Just three settings that need to agree:

  1. package.json: Tell Node.js you’re using ESM
  2. tsconfig.json: Tell TypeScript to output ESM
  3. SDK version: Use one that supports pure ESM

Get those three right, and everything works perfectly.

Get any one wrong, and nothing works at all.

No graceful degradation. No partial functionality. Just complete failure.

This is why systematic debugging matters:

  1. Compare configurations between working and broken
  2. Identify the differences
  3. Apply fixes systematically
  4. Document for next time
  5. Create automated checkers

The Bitcoin testnet took 6 hours to debug the first time.

With this guide, it takes 5 minutes.

That’s the power of documentation.

Now go fix your broken MCP servers. The checklist is waiting. 🚀


Additional Resources

Official Documentation

Tools & Scripts


Technical Tags

esm commonjs mcp-debugging typescript nodejs module-compatibility configuration-management systematic-debugging blockchain-mcp

Metadata

Word Count: ~7,200 words
Code Blocks: 25+
Scripts: 2 (checker + fixer)
Reading Time: 30-35 minutes
Skill Level: Intermediate to Advanced
Prerequisites: TypeScript, Node.js, MCP SDK basics


Next in Series: Blog Post 020 - Vector Embeddings + Ollama: Semantic Search Revolution

Previous in Series: Blog Post 018 - Router DDoS Blacklist Crisis


Hit this issue? Found another edge case? Share your experiences in the comments. Let’s build the definitive ESM/CommonJS compatibility guide together.

Questions? Find me on GitHub or LinkedIn


Prerequisites

Next Steps

Deep Dives