How are you structuring your MCP server's codebase?
Context: as MCP servers grow past a handful of tools, keeping the code organized gets tricky β tool definitions, handlers, and shared state all start competing for structure.
I expected to end up with one file per tool. I ended up with the exact opposite: one file, two blocks.
Our stdio server is freemcp_mcp_server.py β 395 lines, 13 tools. @app.list_tools() declares all 13 in a single list, grouped only by comment headers: Server Management / Template Operations / Usage & Quota / Discovery / Authentication. @app.call_tool() is one long if/elif dispatch chain where each branch is a single line: await _api_request(METHOD, path, arguments).
The shared layer is tiny β a _TokenCache class (asyncio.Lock'd JWT cache) plus _get_headers() and _api_request(). Three helpers and one cache class, that's it.
The reason this stays flat is that the tools carry no business logic at all. They're a thin shell over our REST API; the real logic lives in the backend behind a hexagonal layering (core β domain β ports β adapters β services β api, enforced with import-linter). If your tools contain actual logic, I'd expect this to fall apart much sooner.
The wrinkle: we implement the same tool set twice. Once for stdio (13 tools) and once for hosted SSE/HTTP in api/routers/platform_mcp.py β 240 lines, but only 5 tools (list_servers, deploy_server, delete_server, get_usage, whoami). No code shared between them.
So the honest takeaway: flat single-file dispatch scaled fine to 13 tools. Tool count was never the pain point β re-implementing the same tool set for a second transport was.
What's worked for you? Has anyone found a clean way to share tool definitions across transports?
Posted Aug 11, 2026, 11:24 AM UTC