DEV Community

Gabriele Karra
Gabriele Karra

Posted on

I built a tool that auto-generates MCP servers from any codebase

The Model Context Protocol (MCP) is how AI tools like Claude, Cursor, and
Windsurf connect to external software. You write an MCP server that exposes
"tools", and the AI can call them.

The problem? Writing MCP servers by hand is tedious. You're essentially
translating your app's existing API surface — CLI commands, HTTP endpoints,
Python functions — into MCP tool definitions. For a FastAPI app with 20 routes,
that's a lot of boilerplate.

So I automated it.

What is MCP-Anything?

pip install -e ".[dev]"
mcp-anything generate /path/to/your/app

One command. It scans your project, figures out what it does, and generates a
complete MCP server package — pip-installable, with tests, docs, and a
ready-to-paste config for Claude Code.

GitHub: github.com/gabrielekarra/mcp-anything

How it works

The pipeline has 6 phases:

ANALYZE → DESIGN → IMPLEMENT → TEST → DOCUMENT → PACKAGE

Phase 1: Analyze

Eight detectors scan for IPC mechanisms — how does this app communicate with the
outside world?

  • CLI — argparse, click, typer, fire
  • HTTP frameworks — FastAPI, Flask (Python AST-based)
  • Spring Boot — Java annotations via regex
  • OpenAPI/Swagger — spec file parsing (no source code needed)
  • Sockets, gRPC, file I/O — and more

For Python apps, the analyzer uses the ast module to extract functions,
parameters with types, default values, docstrings, and decorators. No regex hacks
for Python — full AST parsing.

For FastAPI specifically, it understands:

  • @app.get("/users/{user_id}") route decorators
  • Query(), Path(), Body() parameter injection
  • Depends() filtering (skips injected dependencies)
  • APIRouter(prefix="/items") prefix resolution
  • Pydantic models as request body types

Phase 2: Design

Each detected capability gets mapped to an implementation strategy:

┌────────────────┬─────────────────────────────┬─────────────────────────────┐
│ Strategy │ When │ How it works │
├────────────────┼─────────────────────────────┼─────────────────────────────┤
│ http_call │ REST endpoints │ Async HTTP request via │
│ │ │ httpx │
├────────────────┼─────────────────────────────┼─────────────────────────────┤
│ cli_subcommand │ CLI apps │ Subprocess with args │
├────────────────┼─────────────────────────────┼─────────────────────────────┤
│ python_call │ Python libraries │ Direct import and call │
├────────────────┼─────────────────────────────┼─────────────────────────────┤
│ cli_function │ Python functions in CLI │ Wrapped subprocess call │
│ │ apps │ │
└────────────────┴─────────────────────────────┴─────────────────────────────┘

Parameters are categorized as path, query, or body based on their position in the
route and annotations.

Phase 3-6: Generate

Jinja2 templates produce a complete Python package:

mcp-my-app-server/
├── src/my_app/
│ ├── server.py # FastMCP entry point
│ ├── backend.py # Async HTTP/CLI backend
│ └── tools/ # Generated tool modules
├── tests/ # Pytest tests
├── docs/ # API documentation
└── pyproject.toml # Ready to pip install

Every generated Python file is validated with ast.parse() before writing. If the
templates produce invalid syntax, the pipeline fails fast.

Real example: FastAPI app

I tested against tiangolo's full-stack-fastapi-template — a production FastAPI
app with users, items, auth, and multiple routers.

mcp-anything generate ./backend/app --name fastapi-users --no-llm

Output:

Scanning codebase...
Found 27 source files
Running IPC detectors...
Flask/FastAPI Detector: protocol (confidence: 0.95, 28 signals)
Running AST analysis...
AST: 64 functions, 22 classes
Fastapi: 23 HTTP routes
✓ analyze complete
Designed 49 tools
✓ All phases complete

23 HTTP routes extracted across 5 routers (/items/, /users/, /login/, /utils/,
/private/), with correct parameter schemas. The generated tools include things
like:

@server.tool()
async def get_users(
skip: int | None = 0,
limit: int | None = 100,
) -> str:
"""GET /users/ - Retrieve users."""
query_params = {}
if skip is not None:
query_params["skip"] = str(skip)
if limit is not None:
query_params["limit"] = str(limit)
return await backend.request("GET", "/users/", params=query_params)

The SessionDep and CurrentUser dependency-injected params were correctly filtered
out. Only user-facing parameters made it into the tool schema.

Real example: OpenAPI spec (no source code)

You don't even need source code. Point it at a directory with an OpenAPI spec:

mcp-anything generate /path/to/realworld --name conduit-api --no-llm

It found specs/api/openapi.yml via recursive search and extracted 19 endpoints
from the RealWorld Conduit API:

login                POST /users/login - Existing user login
createuser           POST /users - Register a new user
getarticles          GET /articles - Get recent articles globally
createarticle        POST /articles - Create an article
getarticle           GET /articles/{slug} - Get an article
createarticlecomment POST /articles/{slug}/comments - Create a comment
...
Enter fullscreen mode Exit fullscreen mode

With $ref resolution for request body schemas, enum values, and nested path
parameters.

The interesting technical bits

AST-based parameter extraction for FastAPI

FastAPI uses Python type annotations and default values for dependency injection.
A function like:

async def list_users(
session: SessionDep,
current_user: CurrentUser,
skip: int = 0,
limit: int = Query(10, description="Max results"),
):

The analyzer needs to:

  1. Skip session and current_user (dependency-injected types)
  2. Recognize skip as a plain query parameter with default 0
  3. Parse Query(10, description="Max results") to extract the default value and description
  4. Handle Depends(), Path(), Body(), Header(), Cookie() — each one differently

This is all done via Python's ast module — no imports, no runtime inspection, no
dependencies on FastAPI itself.

Java annotation parsing without a Java parser

For Spring Boot, I couldn't use Python's ast module. Instead, it's regex-based
with some tricks:

  • A balanced parenthesis extractor handles nested annotations like @RequestParam(required = false, defaultValue = "10")
  • Generic return types like List are captured with [\w<>,\s]+?
  • Controller-level @RequestMapping("/api") is prepended to method-level paths

OpenAPI $ref resolution

OpenAPI specs reference schemas via $ref: "#/components/schemas/Pet". The
analyzer resolves these by walking the JSON pointer path and inlining the
referenced schema's properties as individual tool parameters — so instead of a
single opaque body: object parameter, you get name: string (required), species:
string, age: integer.

What's missing

  • Authentication — no API key, Bearer token, or OAuth support yet. Generated servers can't auth against protected APIs.
  • More languages — Express.js, Go, Django REST Framework, Rust are on the roadmap.
  • Schema depth — nested object properties aren't fully expanded yet.

See the full ROADMAP for planned features.

Try it

git clone https://github.com/gabrielekarra/mcp-anything.git
cd mcp-anything
pip install -e ".[dev]"

# Try it on your own project
mcp-anything generate /path/to/your/app --no-llm

# Or on an OpenAPI spec
mcp-anything generate /path/to/spec-directory --name my-api --no-llm

162 tests, ~7k lines of code, MIT licensed.

What framework would you want supported next? I'm prioritizing

Top comments (0)