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:
httpx→fetch()@mcp.tool()decorator →this.server.tool()methodFastMCP→ Cloudflare’sMcpAgentwith Durable Objects
~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:
@cloudflare/agents→ deprecatedagents-sdk→ deprecatedagents→ current
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" }]
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:
- Local development
- Claude Desktop integration
- Platforms with full Python support (AWS Lambda, Cloud Run)
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.