MCP Creator — FastMCP v3
Build production-ready MCP servers with FastMCP v3 (3.0.0rc2). This skill guides through research, scaffolding, implementation, testing, and deployment. All output follows this repo's conventions: mcp/<name>/ directory, fastmcp.json config, uv workspace member, imperative voice, kebab-case naming.
Target: FastMCP v3 rc2 — Provider/Transform architecture, 14 built-in middleware, OAuth 2.1, server composition, component versioning, structured output, background tasks, elicitation, sampling.
Input: $ARGUMENTS — the service, API, or capability to wrap as an MCP server.
Dispatch
Route $ARGUMENTS to the appropriate mode:
| Input pattern | Mode | Start at |
|---|---|---|
| Service/API name (e.g., "GitHub", "Stripe") | New server | Phase 1 |
Path to existing server (e.g., mcp/myserver/) | Extend | Phase 3 |
| OpenAPI spec URL or file path | Convert OpenAPI | Phase 2 (scaffold) then load references/server-composition.md §6 |
| FastAPI app to convert | Convert FastAPI | Phase 2 (scaffold) then load references/server-composition.md §7 |
| Error message or "debug" + description | Debug | Load references/common-errors.md, match symptom |
| "learn" or conceptual question | Learn | Load relevant reference file, explain |
| Empty or unclear | Prompt | Ask what service/API to integrate |
Consult Live Documentation
Before implementation, fetch current FastMCP v3 docs. Bundled references capture rc2 — live docs may be newer.
- •Context7 —
resolve-library-idfor "fastmcp", thenquery-docsfor the topic. - •WebFetch —
https://gofastmcp.com/llms-full.txt— comprehensive LLM-optimized docs. - •WebSearch fallback —
site:gofastmcp.com <topic>for specific topics.
If live docs contradict bundled references, live docs win. Always fetch live docs first — API details shift between rc2 and stable.
Phase 1: Research & Plan
Goal: Understand the target service and design the MCP server's tool/resource/prompt inventory.
Load: references/tool-design.md (naming, descriptions, parameter design, 8 tool patterns)
1.1 Understand the Target
- •Read any documentation, SDK, or API reference for the target service.
- •Identify the core operations users need (CRUD, search, status, config).
- •Note authentication requirements (API keys, OAuth, tokens).
- •Check for existing MCP servers for this service — avoid duplicating work.
1.2 Architecture Checklist
Answer before proceeding:
- • Auth needed? (API key → env var, OAuth → HTTP transport, none → stdio)
- • Transport? (stdio for local, Streamable HTTP for remote/multi-client)
- • Background tasks? (long-running operations →
task=True, requiresfastmcp[tasks]) - • OpenAPI spec available? (→
OpenAPIProviderorFastMCP.from_openapi()) - • Multiple domains? (→ composed servers with
mount()+ namespaces)
1.3 Design Tool Inventory
Plan 5-15 tools per server. For each tool, define:
| Field | Requirement |
|---|---|
| Name | snake_case, verb_noun format, max 64 chars |
| Description | 3-5 sentences: WHAT, WHEN to use, WHEN NOT, WHAT it returns |
| Parameters | Each with type, description, constraints via Annotated[type, Field(...)] |
| Annotations | readOnlyHint, destructiveHint, idempotentHint, openWorldHint |
| Error cases | What ToolError messages to raise for expected failures |
1.4 Design Resources and Prompts
- •Resources for static/slow-changing data (config, schemas, status). URI-addressed.
- •Prompts for reusable message templates that guide LLM behavior.
- •See
references/fastmcp-v3-api.md§6-7 for URI patterns and prompt design.
1.5 Plan Architecture
Decide on composition strategy:
- •Single server — Most cases. One
FastMCPinstance with all tools. - •Composed servers — Large APIs. Domain servers mounted via
mount()with namespaces. - •Provider-based — Dynamic tool registration.
FileSystemProvideror customProvider. - •OpenAPI conversion — Auto-generate tools from OpenAPI spec.
OpenAPIProviderorFastMCP.from_openapi().
See references/server-composition.md for patterns.
1.6 Deliverable
Produce a tool/resource/prompt inventory table before proceeding:
| Component | Type | Name | Description (brief) | |-----------|------|------|---------------------| | Tool | tool | search_issues | Search GitHub issues by query | | Resource | resource | config://settings | Current server configuration | | Prompt | prompt | summarize_pr | Summarize a pull request for review |
Phase 2: Scaffold
Goal: Create the project directory, tests, and configure dependencies.
2.1 Create Project Structure
Run wagents new mcp <name> to scaffold:
mcp/<name>/
├── server.py # FastMCP entry point
├── pyproject.toml # uv project config
├── fastmcp.json # FastMCP CLI config
└── tests/
├── conftest.py # Client fixture, mock Context
└── test_server.py # Automated test suite
Customize the scaffold:
- •Set server name in
server.py:mcp = FastMCP("name"). - •Set package name in
pyproject.toml:name = "mcp-<name>". - •Update description in
pyproject.toml. - •Add service-specific dependencies to both
pyproject.tomlandfastmcp.json.
2.2 Add Test Configuration
Add to pyproject.toml:
[tool.pytest.ini_options] asyncio_mode = "auto" [dependency-groups] dev = ["pytest>=8", "pytest-asyncio>=0.25"]
Create tests/conftest.py — see references/testing.md §4 for the complete template.
2.3 Configure and Verify
- •Ensure root
pyproject.tomlhas[tool.uv.workspace] members = ["mcp/*"]. - •Run
cd mcp/<name> && uv syncto install dependencies. - •Verify:
uv run python -c "from server import mcp; print(mcp.name)".
Note: wagents validate validates skills/agents only, NOT MCP servers. Use the import check above for MCP server validation.
Phase 3: Implement
Goal: Build all tools, resources, and prompts from the Phase 1 inventory.
Load: references/fastmcp-v3-api.md (full API surface), references/tool-design.md (patterns)
3.1 Server Setup
from fastmcp import FastMCP, Context
mcp = FastMCP(
"server-name",
instructions="Description of what this server provides and when to use it.",
)
For shared resources (HTTP clients, DB connections), add a composable lifespan:
from fastmcp.server.lifespan import lifespan
@lifespan
async def http_lifespan(server):
import httpx
async with httpx.AsyncClient() as client:
yield {"http": client}
mcp = FastMCP("server-name", lifespan=http_lifespan)
# Access in tools: ctx.lifespan_context["http"]
Combine lifespans with |: mcp = FastMCP("name", lifespan=db_lifespan | cache_lifespan)
3.2 Implement Tools
For each tool in the inventory, follow this pattern:
from typing import Annotated
from pydantic import Field
from fastmcp import Context
from fastmcp.exceptions import ToolError
@mcp.tool(
annotations={
"readOnlyHint": True,
"openWorldHint": True,
},
)
async def search_items(
query: Annotated[str, Field(description="Search term to find items.", min_length=1)],
limit: Annotated[int, Field(description="Max results to return.", ge=1, le=100)] = 10,
ctx: Context | None = None,
) -> dict:
"""Search for items matching a query.
Use this tool when you need to find items by keyword. Returns a list of
matching items with their IDs and titles. Use the limit parameter to
control result count. Does not search archived items.
Returns a dictionary with 'items' list and 'total' count.
"""
if ctx:
await ctx.info(f"Searching for: {query}")
try:
results = await do_search(query, limit)
return {"items": results, "total": len(results)}
except ServiceError as e:
raise ToolError(f"Search failed: {e}")
Key rules for every tool:
- •
Annotated[type, Field(description=...)]on EVERY parameter. - •Verbose docstring: WHAT, WHEN to use, WHEN NOT, WHAT it returns.
- •
ToolErrorfor expected failures (always visible to client). - •
annotationsdict on every tool — at minimumreadOnlyHint. - •
ctx: Context | None = Nonefor testability without MCP runtime.
See references/tool-design.md §9 for 8 complete tool patterns (sync, async+Context, stateful, external API, data processing, dependency-injected, sampling, elicitation).
3.3 Implement Resources
import json
@mcp.resource("config://settings", mime_type="application/json")
async def get_settings() -> str:
"""Current server configuration."""
return json.dumps(settings)
@mcp.resource("users://{user_id}/profile")
async def get_user_profile(user_id: str) -> str:
"""User profile by ID."""
return json.dumps(await fetch_profile(user_id))
See references/fastmcp-v3-api.md §6 for URI templates, query params, wildcards, and class-based resources.
3.4 Implement Prompts
from fastmcp.prompts import Message
@mcp.prompt
def summarize_pr(pr_number: int, detail_level: str = "brief") -> list[Message]:
"""Generate a prompt to summarize a pull request."""
return [Message(
role="user",
content=f"Summarize PR #{pr_number} at {detail_level} detail level.",
)]
3.5 Composition (if applicable)
For large servers, split into domain modules and compose. See references/server-composition.md.
from fastmcp import FastMCP
from .issues import issues_server
from .repos import repos_server
mcp = FastMCP("github")
mcp.mount(issues_server, namespace="issues")
mcp.mount(repos_server, namespace="repos")
3.6 Auth (if applicable)
Load references/auth-and-security.md when implementing authentication.
- •stdio transport: No MCP-level auth. Use env vars for backend API keys.
- •HTTP transport: OAuth 2.1 via
JWTVerifier,GitHubProvider, orRemoteAuthProvider. - •Per-tool auth:
@mcp.tool(auth=require_scopes("admin")). - •Dual-mode pattern:
common.pywith shared tools, separate auth/no-auth entry points.
Phase 4: Test
Goal: Verify all components work correctly with deterministic tests.
Load: references/testing.md (patterns, 18-item checklist)
4.1 Write Tests
Use the in-memory Client — no network, no subprocess:
import pytest
from fastmcp import Client
from server import mcp
@pytest.fixture
async def client():
async with Client(mcp) as c:
yield c
async def test_search_items(client):
result = await client.call_tool("search_items", {"query": "test"})
assert result.data is not None
assert not result.is_error
async def test_list_tools(client):
tools = await client.list_tools()
names = [t.name for t in tools]
assert "search_items" in names
4.2 Test Categories
Cover all 8 categories from references/testing.md:
- •Discovery —
list_tools(),list_resources(),list_prompts()return expected names. - •Happy path — Each tool with valid input returns expected output.
- •Error handling — Invalid input produces
ToolError, not crashes. - •Edge cases — Empty strings, boundary values, Unicode, large inputs.
- •Resources —
read_resource(uri)returns correct content and MIME type. - •Prompts —
get_prompt(name, args)returns expected messages. - •Integration — Tool chains, lifespan setup/teardown.
- •Concurrent — Multiple simultaneous calls don't interfere.
4.3 Interactive Testing
# MCP Inspector (browser-based)
fastmcp dev inspector mcp/<name>/server.py
# CLI testing
fastmcp list mcp/<name>/server.py
fastmcp call mcp/<name>/server.py search_items '{"query": "test"}'
4.4 Run Tests
cd mcp/<name> && uv run pytest -v
Phase 5: Deploy & Configure
Goal: Make the server available to MCP clients.
Load: references/deployment.md (transports, client configs, Docker)
5.1 Select Transport
| Scenario | Transport | Command |
|---|---|---|
| Local / Claude Desktop | stdio | fastmcp run server.py |
| Remote / multi-client | Streamable HTTP | fastmcp run server.py --transport http --port 8000 |
| Development | Inspector | fastmcp dev inspector server.py |
5.2 Generate Client Config
Add to client config (Claude Desktop, Claude Code, Cursor):
{
"mcpServers": {
"server-name": {
"command": "uv",
"args": ["run", "--directory", "/path/to/mcp/<name>", "fastmcp", "run", "server.py"]
}
}
}
See references/deployment.md §7 for complete configs per client.
5.3 Validate
MCP server validation (wagents validate does NOT check MCP servers):
# Import check uv run python -c "from server import mcp; print(mcp.name)" # List registered components fastmcp list mcp/<name>/server.py # Interactive inspection fastmcp dev inspector mcp/<name>/server.py
5.4 Quality Checklist
Before declaring the server complete:
- • Every tool has
Annotated+Field(description=...)on all parameters - • Every tool has a verbose docstring (WHAT, WHEN, WHEN NOT, RETURNS)
- • Every tool has
annotations(at minimumreadOnlyHint) - • Every tool uses
ToolErrorfor expected failures - • No
print()orstdoutwrites in any tool - • Resources have correct URI schemes and MIME types
- • Tests pass:
uv run pytest -v - • MCP Inspector shows all components correctly
- •
fastmcp.jsonlists all required dependencies - •
pyproject.tomlhas correct metadata and dependencies - • No deprecated constructor kwargs (removed in rc1)
- • Custom routes have manual auth if sensitive
- •
mask_error_details=Trueset for production deployment
Reference File Index
Load these files on demand during the relevant phase. Do NOT load all at once.
| File | Content | Load during |
|---|---|---|
references/fastmcp-v3-api.md | Complete v3 API surface: constructor, decorators, Context, return types, resources, prompts, providers, transforms, 14 middleware, background tasks, visibility, v2→v3 changes | Phase 3 |
references/tool-design.md | LLM-optimized naming, descriptions, parameters, annotations, error handling, 8 tool patterns, structured output, response patterns, anti-patterns | Phase 1, 3 |
references/server-composition.md | mount(), import_server(), proxy, FileSystemProvider, OpenAPI (OpenAPIProvider + from_openapi), FastAPI conversion, custom providers, transforms, gateway pattern, DRY registration | Phase 1, 3 |
references/testing.md | In-memory Client, pytest setup, conftest.py template, 8 test categories, MCP Inspector, CLI testing, 18-item checklist | Phase 4 |
references/auth-and-security.md | OAuth 2.1, JWTVerifier, per-component auth, custom auth checks, session-based visibility, custom route auth bypass, SSRF prevention, dual-mode pattern, 15 security rules | Phase 3 |
references/deployment.md | Transports, FastMCP CLI, ASGI, custom routes, client configs, fastmcp.json schema, Docker, background task workers, production checklist | Phase 5 |
references/resources-and-prompts.md | Resources (static, dynamic, binary), prompts (single/multi-message), resource vs tool guidance, testing patterns | Phase 3 |
references/common-errors.md | 34 errors: symptom → cause → v3-updated fix, quick-fix lookup table | Debug mode |
Critical Rules
These are non-negotiable. Violating any of these produces broken MCP servers.
- •
No stdout. Never use
print()or write to stdout in tools/resources/prompts. Stdout is the MCP transport. Usectx.info(),ctx.warning(),ctx.error()for logging. - •
ToolError for expected failures. Always
raise ToolError("message")for user-facing errors. Standard exceptions are masked bymask_error_detailsin production. - •
Verbose descriptions. Every tool needs a 3-5 sentence docstring. Every parameter needs
Field(description=...). LLMs cannot use tools they don't understand. - •
Annotations on every tool. Set
readOnlyHint,destructiveHint,idempotentHint,openWorldHint. Clients use these for confirmation flows and retry logic. - •
No
*argsor**kwargs. MCP requires a fixed JSON schema for tool inputs. Dynamic signatures break schema generation. - •
Async state access. In v3,
ctx.get_state()andctx.set_state()are async — alwaysawaitthem. - •
URI schemes required. Every resource URI must have a scheme (
data://,config://,users://). Bare paths fail. - •
Test deterministically. Use in-memory
Client(mcp), not manual prompting. Tests must be repeatable and automated. - •
Module-level
mcpvariable. TheFastMCPinstance must be importable at module level.fastmcp runimportsserver:mcpby default. - •
Secrets in env vars only. Never hardcode API keys. Never accept tokens as tool parameters. Load from environment, validate on startup.
Quick Reference: FastMCP v3 Essentials
Minimal examples to start without loading any reference file.
Server:
from fastmcp import FastMCP, Context
mcp = FastMCP("my-server", instructions="Server description for clients.")
Tool:
from typing import Annotated
from pydantic import Field
from fastmcp.exceptions import ToolError
@mcp.tool(annotations={"readOnlyHint": True})
async def get_item(
item_id: Annotated[str, Field(description="Unique item identifier.")],
ctx: Context | None = None,
) -> dict:
"""Retrieve an item by its ID.
Use when you need to look up a specific item. Returns the item's
full details including name, status, and metadata. Does not return
archived items — use get_archived_item instead.
"""
if ctx: await ctx.info(f"Fetching item {item_id}")
item = await fetch_item(item_id)
if not item: raise ToolError(f"Item {item_id} not found")
return item
Resource:
@mcp.resource("config://version", mime_type="text/plain")
def get_version() -> str:
"""Current server version."""
return "1.0.0"
Prompt:
from fastmcp.prompts import Message
@mcp.prompt
def analyze(topic: str) -> list[Message]:
"""Generate an analysis prompt."""
return [Message(role="user", content=f"Analyze {topic} in detail.")]
Context (lifespan):
from fastmcp.server.lifespan import lifespan
@lifespan
async def app_lifespan(server):
import httpx
async with httpx.AsyncClient() as client:
yield {"http": client}
mcp = FastMCP("my-server", lifespan=app_lifespan)
@mcp.tool(annotations={"readOnlyHint": True, "openWorldHint": True})
async def fetch_data(
url: Annotated[str, Field(description="URL to fetch data from.")],
ctx: Context | None = None,
) -> str:
"""Fetch data from an external URL."""
http = ctx.lifespan_context["http"]
resp = await http.get(url)
return resp.text
Test:
from fastmcp import Client
from server import mcp
async def test_get_item():
async with Client(mcp) as c:
result = await c.call_tool("get_item", {"item_id": "abc"})
assert result.data is not None
assert not result.is_error
Run:
fastmcp run server.py # stdio (default) fastmcp run server.py --transport http --port 8000 # HTTP fastmcp dev inspector server.py # Inspector UI