AgenticWire test record: fastmcp-vs-fastapi-mcp Article: https://www.agenticwire.news/article/fastmcp-vs-fastapi-mcp Machine and versions: see results.json (machine field). Harness script (experiment/run.sh): #!/usr/bin/env bash # fastmcp vs fastapi-mcp: does a plain install of each actually work today? # # Self-contained: builds its own venvs, assumes only python3 + uv on PATH. # Re-run: node blog-app/scripts/pipeline/bench.mjs drafts/fastmcp-vs-fastapi-mcp set -uo pipefail cd "$(dirname "$0")" log() { echo "$@" >&2; } emit() { echo "$1"; } WORK="${TMPDIR:-/tmp}/famcp-bench-$$" mkdir -p "$WORK" trap 'rm -rf "$WORK"' EXIT log "building venvs in $WORK" uv venv --python 3.13 "$WORK/fmcp" >&2 2>/dev/null uv venv --python 3.13 "$WORK/famcp" >&2 2>/dev/null VIRTUAL_ENV="$WORK/fmcp" uv pip install fastmcp fastapi >&2 2>/dev/null VIRTUAL_ENV="$WORK/famcp" uv pip install fastapi-mcp fastapi >&2 2>/dev/null PY_F="$WORK/fmcp/bin/python" PY_A="$WORK/famcp/bin/python" # --- versions, from a plain install today --- V_FMCP=$($PY_F -c "import importlib.metadata as m; print(m.version('fastmcp'))") V_FAMCP=$($PY_A -c "import importlib.metadata as m; print(m.version('fastapi-mcp'))") V_MCP_IN_FAMCP=$($PY_A -c "import importlib.metadata as m; print(m.version('mcp'))") emit "{\"metric\":\"fastmcp_version_resolved\",\"value\":\"$V_FMCP\",\"note\":\"uv pip install fastmcp\"}" emit "{\"metric\":\"fastapi_mcp_version_resolved\",\"value\":\"$V_FAMCP\",\"note\":\"uv pip install fastapi-mcp\"}" emit "{\"metric\":\"mcp_version_pulled_by_fastapi_mcp\",\"value\":\"$V_MCP_IN_FAMCP\",\"note\":\"transitive dep, fastapi-mcp declares mcp>=1.12.0 with no upper bound\"}" # --- days since fastapi-mcp's last PyPI release (free, public API) --- UPLOAD=$(curl -sS -m 20 https://pypi.org/pypi/fastapi-mcp/json | python3 -c "import json,sys; print(json.load(sys.stdin)['urls'][0]['upload_time'])" 2>/dev/null || echo "") if [ -n "$UPLOAD" ]; then DAYS=$(python3 -c " from datetime import datetime u = datetime.fromisoformat('$UPLOAD') print((datetime.utcnow() - u).days) ") emit "{\"metric\":\"fastapi_mcp_days_since_last_release\",\"value\":$DAYS,\"unit\":\"days\",\"note\":\"PyPI upload_time for $V_FAMCP was $UPLOAD\"}" fi # --- does a fresh install of fastapi-mcp actually work? (the finding) --- cat > "$WORK/conv_famcp.py" <<'PY' from fastapi import FastAPI from fastapi_mcp import FastApiMCP app = FastAPI() @app.get("/items/{item_id}") def get_item(item_id: int): return {"item_id": item_id} mcp = FastApiMCP(app) mcp.mount() print("OK", "/mcp" in [r.path for r in app.routes]) PY if $PY_A "$WORK/conv_famcp.py" >"$WORK/famcp_fresh.out" 2>"$WORK/famcp_fresh.err"; then emit "{\"metric\":\"fastapi_mcp_fresh_install_works\",\"value\":true}" else ERR=$(tail -1 "$WORK/famcp_fresh.err" | sed 's/"/\\"/g') emit "{\"metric\":\"fastapi_mcp_fresh_install_works\",\"value\":false,\"note\":\"$ERR\"}" fi # --- pin mcp<2 (the constraint fastapi-mcp's own metadata technically allows): does that fix it? --- VIRTUAL_ENV="$WORK/famcp" uv pip install "mcp<2" >&2 2>/dev/null V_MCP_PINNED=$($PY_A -c "import importlib.metadata as m; print(m.version('mcp'))") if $PY_A "$WORK/conv_famcp.py" >"$WORK/famcp_pinned.out" 2>"$WORK/famcp_pinned.err"; then emit "{\"metric\":\"fastapi_mcp_works_with_mcp_lt_2\",\"value\":true,\"note\":\"pinned mcp==$V_MCP_PINNED\"}" else emit "{\"metric\":\"fastapi_mcp_works_with_mcp_lt_2\",\"value\":false}" fi if grep -qi "deprecat" "$WORK/famcp_pinned.err" 2>/dev/null; then emit "{\"metric\":\"fastapi_mcp_mount_deprecation_warning\",\"value\":true,\"note\":\"$(grep -i deprecat "$WORK/famcp_pinned.err" | head -1 | sed 's/\"/\\\"/g')\"}" fi # --- fastmcp.from_fastapi() conversion --- cat > "$WORK/conv_fmcp.py" <<'PY' import asyncio from fastapi import FastAPI from fastmcp import FastMCP app = FastAPI() @app.get("/items/{item_id}") def get_item(item_id: int): return {"item_id": item_id} mcp = FastMCP.from_fastapi(app=app) tools = asyncio.run(mcp.list_tools()) print(len(tools), [t.name for t in tools]) PY if OUT=$($PY_F "$WORK/conv_fmcp.py" 2>&1); then N=$(echo "$OUT" | head -1 | cut -d' ' -f1) emit "{\"metric\":\"fastmcp_from_fastapi_works\",\"value\":true,\"note\":\"converted $N tool(s) from a single-route FastAPI app\"}" else emit "{\"metric\":\"fastmcp_from_fastapi_works\",\"value\":false}" fi # --- OAuth provider surface: article claims 3 dedicated + "documented compatibility" with others --- N_PROVIDERS=$($PY_F -c " import pkgutil, fastmcp.server.auth.providers as p print(len([m for m in pkgutil.iter_modules(p.__path__)])) ") emit "{\"metric\":\"fastmcp_dedicated_oauth_providers\",\"value\":$N_PROVIDERS,\"note\":\"submodules under fastmcp.server.auth.providers\"}" if $PY_A -c "from fastapi_mcp import AuthConfig" >/dev/null 2>&1; then R=true; else R=false; fi emit "{\"metric\":\"fastapi_mcp_has_dedicated_provider_layer\",\"value\":$R,\"note\":\"AuthConfig relies on FastAPI's own dependency-injected auth, not named OAuth providers\"}" log "done"