← All writing← 全部文章 Essay · 2025-12-27 · 3 min read

Why I Had to Rewrite My MCP Server from Python to TypeScript

I tried deploying a Python MCP server to Cloudflare Workers. It didn't work. Here's what I learned about remote MCP deployment.

这篇暂时只有英文版。

I had a working Python MCP server with 24 tools for accessing Fannie Mae mortgage data. I wanted to deploy it remotely so Claude could access it from anywhere—not just my local machine.

Cloudflare Workers seemed perfect. They’d just announced Python support. Easy, right?

It wasn’t.


The Python Attempt

My server used httpx for API calls and the mcp SDK with FastMCP. Standard stuff:

@mcp.tool()
async def get_loan_limits_by_location(state: str, county: str) -> dict:
    return await client.make_request(f"/v1/loan-limits/state/{state}/county/{county}")

I configured wrangler.jsonc for Python Workers, ran pywrangler deploy, and… the health check worked!

curl https://my-mcp-server.username.workers.dev/health
# {"status": "healthy", "server": "My MCP Server"}

Then I hit the MCP endpoint:

PythonError: Pyodide fatal error: Cannot import anyio

The Wall

Cloudflare’s Python Workers run on Pyodide—Python compiled to WebAssembly. The problem: MCP SDK depends on starlette, which depends on anyio for async handling.

anyio doesn’t work in WebAssembly.

The health check worked because it’s just a simple JSON response. But the moment Pyodide tried to load the MCP server’s async dependencies, it crashed.

I tried various workarounds—different compatibility flags, lazy imports, alternative patterns. Nothing worked. The async stack MCP needs simply isn’t compatible with Pyodide’s runtime.


The Rewrite

TypeScript it is. The conversion was straightforward—same 24 tools, different syntax:

Python:

@mcp.tool()
async def get_loan_limits_by_location(state: str, county: str) -> dict:
    return await client.make_request(f"/v1/loan-limits/state/{state}/county/{county}")

TypeScript:

this.server.tool(
  "get_loan_limits_by_location",
  {
    state: z.string().describe("Two-letter state code"),
    county: z.string().describe("County name")
  },
  async ({ state, county }) => {
    const data = await this.client.makeRequest(`/v1/loan-limits/state/${state}/county/${county}`);
    return { content: [{ type: "text", text: JSON.stringify(data) }] };
  }
);

Key conversions:

~800 lines of TypeScript replaced ~830 lines of Python.


TypeScript Had Its Own Surprises

Even after rewriting, deployment wasn’t smooth:

1. Node.js compatibility flag

The agents package uses Node APIs. Without nodejs_compat, you get:

The package "node:async_hooks" wasn't found on the file system

Fix: Add to wrangler.toml:

compatibility_flags = ["nodejs_compat"]

2. Package naming confusion

The MCP agent package has been renamed multiple times:

I installed the wrong one twice before finding the right package.

3. Durable Object migrations

My Python deployment created a Durable Object class called FannieMaeMCPServer. The TypeScript version used FannieMaeMCP. Cloudflare refused to deploy:

New version does not export class 'FannieMaeMCPServer' which is depended on by existing Durable Objects

Fix: Add a rename migration:

[[migrations]]
tag = "v2"
renamed_classes = [{ from = "FannieMaeMCPServer", to = "FannieMaeMCP" }]

Same server, a plug the socket accepts. Tap a part.服务器没变,换了个插得进去的插头。点一下看每个部件。

The Result

After the rewrite, everything works:

# Health check
curl https://my-mcp-server.username.workers.dev/health
# {"status": "healthy", "server": "My MCP Server"}

# MCP tools list - 26 tools registered
# Live API call - LA County 2025 loan limits
# 1 Unit: $1,209,750
# 2 Units: $1,548,975

26 MCP tools accessing Fannie Mae’s public APIs for loan limits, economic forecasts, housing indicators, and opportunity zones—all callable from Claude anywhere.


The Lesson

For remote MCP servers on Cloudflare Workers, TypeScript is currently the only viable path.

Python MCP works great for:

But if you want Cloudflare’s edge deployment with Durable Objects and SSE transport, you need TypeScript.

The good news: the conversion is mechanical. Same tools, same logic, different syntax. If you have a working Python MCP server, you can port it in an afternoon.

Disagree? Push back on this essay.不同意?来反驳这篇。

Ask MJ

Answers come only from what I’ve written. If it isn’t there, it says so.只根据我写过的内容回答;没写过的,它会说不知道。