mcpbeat

MCP Builder

jezweb/mcp-builder

Build MCP servers in Python with FastMCP. Define tools / resources / prompts, build the server, test locally, deploy to FastMCP Cloud or Docker. Use whenever the user mentions building an MCP server, exposing tools to LLMs, FastMCP, building a Claude integration, or troubleshooting FastMCP module-level server, storage, lifespan, middleware, OAuth, or deployment errors.

27k tokens
context cost
the whole folder, loaded on every use
15
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
953
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/jezweb/claude-skills --skill mcp-builder

The instruction itself

27 sections, as written by the author

MCP Builder

Build a working MCP server from a description of the tools you need. Produces a deployable Python server using FastMCP.

Workflow

Step 1: Define What to Expose

Ask what the server needs to provide:

  • Tools -- Functions Claude can call (API wrappers, calculations, file operations)
  • Resources -- Data Claude can read (database records, config, documents)
  • Prompts -- Reusable prompt templates with parameters

A brief like "MCP server for querying our customer database" is enough.

Step 2: Scaffold the Server

pip install fastmcp

Create the server file. The server instance MUST be at module level:

from fastmcp import FastMCP

# MUST be at module level for FastMCP Cloud
mcp = FastMCP("My Server")

@mcp.tool()
async def search_customers(query: str) -> str:
    """Search customers by name or email."""
    # Implementation here
    return f"Found customers matching: {query}"

@mcp.resource("customers://{customer_id}")
async def get_customer(customer_id: str) -> str:
    """Get customer details by ID."""
    return f"Customer {customer_id} details"

if __name__ == "__main__":
    mcp.run()

Step 3: Add Companion CLI Scripts (Optional)

For Claude Code terminal use, add scripts alongside the MCP server:

my-mcp-server/
├── src/index.ts          # MCP server (for Claude.ai)
├── scripts/
│   ├── search.ts         # CLI version of search tool
│   └── _shared.ts        # Shared auth/config
├── SCRIPTS.md            # Documents available scripts
└── package.json

CLI scripts provide file I/O, batch processing, and richer output that MCP can't.

See assets/SCRIPTS-TEMPLATE.md and assets/script-template.ts for TypeScript templates.

Step 4: Test Locally

Quick test -- run directly:

python server.py

Dev mode with inspector UI (recommended):

fastmcp dev server.py
# Opens inspector at http://localhost:5173
# Hot reload, detailed logging, tool/resource inspection

HTTP mode for remote clients:

python server.py --transport http --port 8000

Automated test script using FastMCP Client:

import asyncio
from fastmcp import Client

async def test_server(server_path):
    async with Client(server_path) as client:
        # List everything
        tools = await client.list_tools()
        resources = await client.list_resources()
        prompts = await client.list_prompts()

        print(f"Tools: {[t.name for t in tools]}")
        print(f"Resources: {[r.uri for r in resources]}")
        print(f"Prompts: {[p.name for p in prompts]}")

        # Call first tool
        if tools:
            result = await client.call_tool(tools[0].name, {})
            print(f"Tool result: {result}")

        # Read first resource
        if resources:
            data = await client.read_resource(resources[0].uri)
            print(f"Resource data: {data}")

asyncio.run(test_server("server.py"))

Step 5: Pre-Deploy Checklist

Run these checks before deploying. All required checks must pass.

Required (will cause deploy failure):

  • Server file exists
  • Python syntax valid: python3 -m py_compile server.py
  • Module-level server object (not inside a function):
   grep -q "^mcp = FastMCP\|^server = FastMCP\|^app = FastMCP" server.py
  • requirements.txt exists with PyPI packages only (no git+, -e, .whl, .tar.gz)
  • No hardcoded secrets (check for api_key = "..." patterns excluding os.getenv/os.environ)

Advisory (warnings):

  • fastmcp listed in requirements.txt
  • .gitignore includes .env
  • No circular imports
  • Git repository initialised with remote

10. Server can load: timeout 5 fastmcp inspect server.py

Step 6: Deploy

FastMCP Cloud (simplest):

git add . && git commit -m "Ready for deployment"
git push -u origin main
# Visit https://fastmcp.cloud, connect repo, add env vars, deploy
# URL: https://your-project.fastmcp.app/mcp

Cloud requirements:

  • Module-level server object named mcp, server, or app
  • PyPI dependencies only in requirements.txt
  • Public GitHub repository
  • Environment variables for secrets (no hardcoded values)
  • Auto-deploys on push to main, PR preview deployments

Docker (self-hosted):

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "server.py", "--transport", "http", "--port", "8000"]

Cloudflare Workers (edge):

See the cloudflare-worker-builder skill for Workers-based MCP servers.


Critical Patterns

Module-Level Server Instance

FastMCP Cloud requires the server instance at module level:

# CORRECT
mcp = FastMCP("My Server")

@mcp.tool()
def my_tool(): ...

# WRONG -- Cloud can't find the server
def create_server():
    mcp = FastMCP("My Server")
    return mcp

# FIX for factory pattern -- export at module level
def create_server() -> FastMCP:
    mcp = FastMCP("server")
    return mcp
mcp = create_server()

Type Annotations Required

FastMCP uses type annotations to generate tool schemas:

@mcp.tool()
async def search(
    query: str,           # Required parameter
    limit: int = 10,      # Optional with default
    tags: list[str] = []  # Complex types supported
) -> str:
    """Docstring becomes the tool description."""
    ...

Error Handling

Return errors as strings, don't raise exceptions:

@mcp.tool()
async def get_data(id: str) -> str:
    try:
        result = await fetch_data(id)
        return json.dumps(result)
    except NotFoundError:
        return f"Error: No data found for ID {id}"

Cloud-Ready Server Pattern

import os
from fastmcp import FastMCP

mcp = FastMCP("production-server")
API_KEY = os.getenv("API_KEY")

@mcp.tool()
async def production_tool(data: str) -> dict:
    if not API_KEY:
        return {"error": "API_KEY not configured"}
    return {"status": "success", "data": data}

if __name__ == "__main__":
    mcp.run()

Common Errors and Fixes

These are the errors you will hit. Fix them before deploying.

| Error | Cause | Fix |

|-------|-------|-----|

| RuntimeError: No server object found at module level | Server inside a function | Export mcp = FastMCP(...) at module level |

| RuntimeError: no running event loop | Missing async/await | Use async def for async operations |

| TypeError: missing required argument 'context' | Context not type-hinted | Add context: Context with type hint |

| ValueError: Invalid resource URI | Missing URI scheme | Use data://, file://, info://, api:// |

| Resource template parameter mismatch | Name mismatch | user://{user_id} needs def get_user(user_id: str) |

| Pydantic validation error | Wrong type hints | Ensure hints match actual data types |

| Transport mismatch | Client/server protocol differ | Match both to stdio or both to http |

| Import errors with editable package | Package not installed | pip install -e . or add to PYTHONPATH |

| DeprecationWarning: mcp.settings | Old API | Use os.getenv() instead |

| Port already in use | Stale process | lsof -ti:8000 \| xargs kill -9 |

| Schema generation failure | Non-JSON types | Use JSON-compatible types (no NumPy arrays) |

| JSON serialization error | datetime/bytes in response | Convert to .isoformat() or string |

| Circular import | Factory in __init__.py | Use direct imports, avoid factory pattern |

| Python 3.12+ datetime warning | datetime.utcnow() deprecated | Use datetime.now(timezone.utc) |

| Import-time execution | Async resource at module level | Use lazy init pattern |


Production Patterns

Self-Contained Server

Keep all utilities in one file to avoid circular imports:

from fastmcp import FastMCP
import os

mcp = FastMCP("my-server")

# Config
class Config:
    API_KEY = os.getenv("API_KEY", "")
    BASE_URL = os.getenv("BASE_URL", "https://api.example.com")

# Helpers
def format_success(data): return {"status": "success", "data": data}
def format_error(msg): return {"status": "error", "message": msg}

@mcp.tool()
async def my_tool(query: str) -> dict:
    if not Config.API_KEY:
        return format_error("API_KEY not configured")
    return format_success({"query": query})

Lazy Initialisation

Don't create async resources at module level. Initialise on first use:

_db = None

async def get_db():
    global _db
    if _db is None:
        _db = await create_connection(Config.DB_URL)
    return _db

Health Check Resource

@mcp.resource("health://status")
async def health_check() -> dict:
    return {
        "status": "healthy",
        "version": "1.0.0",
        "checks": {
            "api": "connected",
            "database": "connected"
        }
    }

Connection Pooling

import httpx

_client = None

def get_client() -> httpx.AsyncClient:
    global _client
    if _client is None:
        _client = httpx.AsyncClient(
            base_url=Config.BASE_URL,
            headers={"Authorization": f"Bearer {Config.API_KEY}"},
            limits=httpx.Limits(max_connections=20, max_keepalive_connections=5),
            timeout=30.0
        )
    return _client

Retry with Backoff

async def retry_with_backoff(func, max_retries=3, initial_delay=1.0):
    for attempt in range(max_retries):
        try:
            return await func()
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            delay = initial_delay * (2 ** attempt)
            await asyncio.sleep(delay)

Context Features (Advanced)

Context Injection

from fastmcp import Context

@mcp.tool()
async def tool_with_context(param: str, context: Context) -> dict:
    # Context parameter MUST have type hint
    pass

Progress Tracking

@mcp.tool()
async def long_task(items: list[str], context: Context) -> str:
    for i, item in enumerate(items):
        await context.report_progress(i + 1, len(items), f"Processing {item}")
        await process(item)
    return "Done"

Sampling (LLM from within tools)

@mcp.tool()
async def summarise(text: str, context: Context) -> str:
    result = await context.request_sampling(
        messages=[{"role": "user", "content": f"Summarise: {text}"}],
        max_tokens=200
    )
    return result

CLI Quick Reference

fastmcp dev server.py              # Dev mode with inspector UI
fastmcp run server.py              # Run (stdio)
fastmcp run server.py --transport http --port 8000  # Run (HTTP)
fastmcp inspect server.py          # Inspect without running
fastmcp install server.py          # Install to Claude Desktop
fastmcp deploy server.py --name my-server  # Deploy to Cloud

Environment variables: FASTMCP_LOG_LEVEL (DEBUG/INFO/WARNING/ERROR), FASTMCP_ENV (development/staging/production).


Integration Patterns (Optional)

For specific integration approaches, see references/integration-patterns.md:

  • Manual API -- httpx.AsyncClient with reusable client
  • OpenAPI auto-generation -- FastMCP.from_openapi(spec, client, route_maps=[...])
  • FastAPI conversion -- FastMCP.from_fastapi(app)

Asset Files

  • assets/basic-server.py -- Minimal FastMCP server template
  • assets/self-contained-server.py -- Server with storage and middleware
  • assets/tools-examples.py -- Tool patterns and type annotations
  • assets/resources-examples.py -- Resource URI patterns
  • assets/prompts-examples.py -- Prompt template patterns
  • assets/client-example.py -- MCP client usage
  • assets/SCRIPTS-TEMPLATE.md -- CLI companion docs template
  • assets/script-template.ts -- TypeScript CLI script template

How to use it

Copy the folder

Take jezweb/mcp-builder from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.

Install what it needs

The instructions reference pip. Without those the skill loads but fails at the first command.