Add development docs for FastMCP (#1719)

Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
This commit is contained in:
Jeremiah Lowin 2025-09-03 11:34:43 -04:00 committed by GitHub
commit c5c6a3ee06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 847 additions and 407 deletions

View file

@ -48,6 +48,15 @@ When modifying MCP functionality, changes typically need to be applied across al
- **Resource Templates** (`src/resources/` + `ResourceManager`)
- **Prompts** (`src/prompts/` + `PromptManager`)
## Writing Style
- Be brief and to the point. Do not regurgitate information that can easily be gleaned from the code, except to guide the reader to where the code is located.
- **NEVER** use "This isn't..." or "not just..." constructions. State what something IS directly. Avoid defensive writing patterns like:
- "This isn't X, it's Y" or "Not just X, but Y" → Just say "This is Y"
- "Not just about X" → State the actual purpose
- "We're not doing X, we're doing Y" → Just explain what you're doing
- Any variation of explaining what something isn't before what it is
## Testing Best Practices
### Testing Standards
@ -59,6 +68,10 @@ When modifying MCP functionality, changes typically need to be applied across al
- **NEVER** add `@pytest.mark.asyncio` to tests - `asyncio_mode = "auto"` is set globally
- **ALWAYS** run pytest after significant changes
### Inline Snapshots
FastMCP uses `inline-snapshot` for testing complex data structures. On first run with empty `snapshot()`, pytest will auto-populate the expected value. To update snapshots after intentional changes, run `pytest --inline-snapshot=fix`. This is particularly useful for testing JSON schemas and API responses.
### Always Use In-Memory Transport
Pass FastMCP servers directly to clients for testing:
@ -203,6 +216,7 @@ If something needs work, your review should help it get there through specific,
### Review Checklist
Before approving, verify:
- [ ] All required development workflow steps completed (uv sync, pre-commit, pytest)
- [ ] Changes align with repository patterns and conventions
- [ ] API changes are documented and backwards-compatible where possible

View file

@ -194,7 +194,7 @@ MCP_AUTH_TOKEN=secret uvicorn app:app --host 0.0.0.0 --port 8000
## Testing Your Deployment
Once your server is deployed, you'll need to verify it's accessible and functioning correctly. For comprehensive testing strategies including connectivity tests, client testing, and authentication testing, see the [Testing Your Server](/deployment/testing) guide.
Once your server is deployed, you'll need to verify it's accessible and functioning correctly. For comprehensive testing strategies including connectivity tests, client testing, and authentication testing, see the [Testing Your Server](/development/tests) guide.
## Hosting Your Server

View file

@ -1,163 +0,0 @@
---
title: Testing Your Server
sidebarTitle: Testing
description: Unit test your MCP servers with the FastMCP Client's deterministic testing capabilities
icon: vial
---
The [FastMCP Client](/clients/client) is a deterministic testing tool that gives you complete programmatic control over MCP server interactions. You call specific tools with exact arguments, verify responses, and test edge cases - making it ideal for unit testing your MCP servers.
## In-Memory Testing
The FastMCP Client's standout feature is in-memory testing. Instead of deploying your server or managing network connections, you pass your server instance directly to the client. This creates a zero-overhead connection that runs entirely in memory.
What makes this approach so powerful is that everything runs in the same Python process. You can set breakpoints anywhere - in your test code or inside your server handlers - and step through with your debugger. There's no server startup scripts, no port management, no cleanup between tests. Tests execute instantly without network overhead.
```python
from fastmcp import FastMCP, Client
# Create your server
server = FastMCP("WeatherServer")
@server.tool
def get_temperature(city: str) -> dict:
"""Get current temperature for a city"""
temps = {"NYC": 72, "LA": 85, "Chicago": 68}
return {"city": city, "temp": temps.get(city, 70)}
@server.resource("weather://forecast")
def get_forecast() -> dict:
"""Get 5-day forecast"""
return {"days": 5, "conditions": "sunny"}
async def test_weather_operations():
# Pass server directly - no deployment needed
async with Client(server) as client:
# Test tool execution
result = await client.call_tool("get_temperature", {"city": "NYC"})
assert result.data == {"city": "NYC", "temp": 72}
# Test resource retrieval
forecast = await client.read_resource("weather://forecast")
assert forecast.contents[0].data == {"days": 5, "conditions": "sunny"}
```
The in-memory approach transforms MCP testing from a deployment challenge into standard unit testing. You focus on testing your server's behavior, not wrestling with infrastructure.
## Testing with Frameworks
The FastMCP Client works seamlessly with any Python testing framework. Whether you prefer pytest, unittest, or another framework, the pattern remains consistent: create a server, pass it to the client, and verify behavior.
```python
import pytest
from fastmcp import FastMCP, Client
@pytest.fixture
def weather_server():
server = FastMCP("WeatherServer")
@server.tool
def get_temperature(city: str) -> dict:
temps = {"NYC": 72, "LA": 85, "Chicago": 68}
return {"city": city, "temp": temps.get(city, 70)}
return server
@pytest.mark.asyncio
async def test_temperature_tool(weather_server):
async with Client(weather_server) as client:
result = await client.call_tool("get_temperature", {"city": "LA"})
assert result.data == {"city": "LA", "temp": 85}
@pytest.mark.asyncio
async def test_unknown_city(weather_server):
async with Client(weather_server) as client:
result = await client.call_tool("get_temperature", {"city": "Paris"})
assert result.data["temp"] == 70 # Default temperature
```
## Mocking External Dependencies
FastMCP servers are standard Python objects, so you can mock external dependencies using your preferred mocking approach. Replace databases, APIs, or any external service with test doubles to keep your tests fast and deterministic.
```python
from unittest.mock import AsyncMock
async def test_database_tool():
server = FastMCP("DataServer")
# Mock the database
mock_db = AsyncMock()
mock_db.fetch_users.return_value = [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
]
@server.tool
async def list_users() -> list:
return await mock_db.fetch_users()
async with Client(server) as client:
result = await client.call_tool("list_users", {})
assert len(result.data) == 2
assert result.data[0]["name"] == "Alice"
mock_db.fetch_users.assert_called_once()
```
## Testing Deployed Servers
While in-memory testing covers most unit testing needs, you'll occasionally need to test against a deployed server - to verify authentication, test network behavior, or validate deployments.
### HTTP Transport Testing
When you need to test actual network behavior or verify a deployment, connect to your running server using its URL:
```python
from fastmcp import Client
async def test_deployed_server():
# Connect to a running server
async with Client("http://localhost:8000/mcp/") as client:
await client.ping()
# Test with real network transport
tools = await client.list_tools()
assert len(tools) > 0
result = await client.call_tool("greet", {"name": "World"})
assert "Hello" in result.data
```
### Testing Authentication
The FastMCP Client handles authentication transparently, making it easy to test secured servers:
```python
from fastmcp.client.transports import StreamableHttpTransport
async def test_authenticated_server():
# Bearer token authentication
async with Client(
StreamableHttpTransport(
"https://api.example.com/mcp",
headers={"Authorization": "Bearer test-token"}
)
) as client:
await client.ping()
tools = await client.list_tools()
# OAuth flow (opens browser for authorization)
async with Client("https://api.example.com/mcp", auth="oauth") as client:
result = await client.call_tool("protected_tool", {})
assert result.data is not None
```
## Best Practices
1. **Default to in-memory testing** - It's faster, more reliable, and easier to debug
2. **Test behavior, not implementation** - Call tools and verify responses rather than testing internals
3. **Use framework fixtures** - Create reusable server configurations for your test suite
4. **Mock external dependencies** - Keep tests fast and deterministic by mocking databases, APIs, etc.
5. **Test error cases** - Verify your server handles invalid inputs and edge cases properly
The FastMCP Client transforms MCP server testing from a deployment challenge into a straightforward unit testing task. With in-memory connections and deterministic control, you can build comprehensive test suites that run in milliseconds.

View file

@ -0,0 +1,187 @@
---
title: "Contributing"
description: "Development workflow for FastMCP contributors"
icon: code-pull-request
---
Contributing to FastMCP means joining a community that values clean, maintainable code and thoughtful API design. All contributions are valued - from fixing typos in documentation to implementing major features.
## Issues
### Issue First, Code Second
**Every pull request requires a corresponding issue - no exceptions.** This requirement creates a collaborative space where approach, scope, and alignment are established before code is written. Issues serve as design documents where maintainers and contributors discuss implementation strategy, identify potential conflicts with existing patterns, and ensure proposed changes advance FastMCP's vision.
**FastMCP is an opinionated framework, not a kitchen sink.** The maintainers have strong beliefs about what FastMCP should and shouldn't do. Just because something takes N lines of code and you want it in fewer lines doesn't mean FastMCP should take on the maintenance burden or endorse that pattern. This is judged at the maintainers' discretion.
Use issues to understand scope BEFORE opening PRs. The issue discussion determines whether a feature belongs in core, contrib, or not at all.
### Writing Good Issues
FastMCP is an extremely highly-trafficked repository maintained by a very small team. Issues that appear to transfer burden to maintainers without any effort to validate the problem will be closed. Please help the maintainers help you by always providing a minimal reproducible example and clearly describing the problem.
**LLM-generated issues will be closed immediately.** Issues that contain paragraphs of unnecessary explanation, verbose problem descriptions, or obvious LLM authorship patterns obfuscate the actual problem and transfer burden to maintainers.
Write clear, concise issues that:
- State the problem directly
- Provide a minimal reproducible example
- Skip unnecessary background or context
- Take responsibility for clear communication
Issues may be labeled "Invalid" simply due to confusion caused by verbosity or not adhering to the guidelines outlined here.
## Pull Requests
PRs that deviate from FastMCP's core principles will be rejected regardless of implementation quality. **PRs are NOT for iterating on ideas** - they should only be opened for ideas that already have a bias toward acceptance based on issue discussion.
### Development Environment
#### Installation
To contribute to FastMCP, you'll need to set up a development environment with all necessary tools and dependencies.
```bash
# Clone the repository
git clone https://github.com/jlowin/fastmcp.git
cd fastmcp
# Install all dependencies including dev tools
uv sync
# Install pre-commit hooks
uv run pre-commit install
```
In addition, some development commands require [just](https://github.com/casey/just) to be installed.
Pre-commit hooks will run automatically on every commit to catch issues before they reach CI. If you see failures, fix them before committing - never commit broken code expecting to fix it later.
### Development Standards
#### Scope
Large pull requests create review bottlenecks and quality risks. Unless you're fixing a discrete bug or making an incredibly well-scoped change, keep PRs small and focused.
A PR that changes 50 lines across 3 files can be thoroughly reviewed in minutes. A PR that changes 500 lines across 20 files requires hours of careful analysis and often hides subtle issues.
Breaking large features into smaller PRs:
- Creates better review experiences
- Makes git history clear
- Simplifies debugging with bisect
- Reduces merge conflicts
- Gets your code merged faster
#### Code Quality
FastMCP values clarity over cleverness. Every line you write will be maintained by someone else - possibly years from now, possibly without context about your decisions.
**PRs can be rejected for two opposing reasons:**
1. **Insufficient quality** - Code that doesn't meet our standards for clarity, maintainability, or idiomaticity
2. **Overengineering** - Code that is overbearing, unnecessarily complex, or tries to be too clever
The focus is on idiomatic, high-quality Python. FastMCP uses patterns like `NotSet` type as an alternative to `None` in certain situations - follow existing patterns.
#### Required Practices
**Full type annotations** on all functions and methods. They catch bugs before runtime and serve as inline documentation.
**Async/await patterns** for all I/O operations. Even if your specific use case doesn't need concurrency, consistency means users can compose features without worrying about blocking operations.
**Descriptive names** make code self-documenting. `auth_token` is clear; `tok` requires mental translation.
**Specific exception types** make error handling predictable. Catching `ValueError` tells readers exactly what error you expect. Never use bare `except` clauses.
#### Anti-Patterns to Avoid
**Complex one-liners** are hard to debug and modify. Break operations into clear steps.
**Mutable default arguments** cause subtle bugs. Use `None` as the default and create the mutable object inside the function.
**Breaking established patterns** confuses readers. If you must deviate, discuss in the issue first.
### Pre-Commit Checks
```bash
# Runs automatically on commit, or manually:
uv run pre-commit run --all-files
```
This runs three critical tools:
- **Ruff**: Linting and formatting
- **ty**: Static type checking
- **Pytest**: Core test suite
CI will reject PRs that fail these checks. Always run them locally first.
### Testing
Tests are documentation that shows how features work. Good tests give reviewers confidence and help future maintainers understand intent.
```bash
# Run specific test directory
uv run pytest tests/server/ -v
# Run all tests before submitting PR
uv run pytest
```
Every new feature needs tests. See the [Testing Guide](/development/tests) for patterns and requirements.
### Documentation
A feature doesn't exist unless it's documented. Note that FastMCP's hosted documentation always tracks the main branch - users who want historical documentation can clone the repo, checkout a specific tag, and host it themselves.
```bash
# Preview documentation locally
just docs
```
Documentation requirements:
- **Explain concepts in prose first** - Code without context is just syntax
- **Complete, runnable examples** - Every code block should be copy-pasteable
- **Register in docs.json** - Makes pages appear in navigation
- **Version badges** - Mark when features were added using `<VersionBadge />`
#### SDK Documentation
FastMCP's SDK documentation is auto-generated from the source code docstrings and type annotations. It is automatically updated on every merge to main by a GitHub Actions workflow, so users are *not* responsible for keeping the documentation up to date. However, to generate it proactively, you can use the following command:
```bash
just api-ref-all
```
### Submitting Your PR
#### Before Submitting
1. **Run all checks**: `uv run pre-commit run --all-files && uv run pytest`
2. **Keep scope small**: One feature or fix per PR
3. **Write clear description**: Your PR description becomes permanent documentation
4. **Update docs**: Include documentation for API changes
#### PR Description
Write PR descriptions that explain:
- What problem you're solving
- Why you chose this approach
- Any trade-offs or alternatives considered
- Migration path for breaking changes
Focus on the "why" - the code shows the "what". Keep it concise but complete.
#### What We Look For
**Framework Philosophy**: FastMCP is NOT trying to do all things or provide all shortcuts. Features are rejected when they don't align with the framework's vision, even if perfectly implemented. The burden of proof is on the PR to demonstrate value.
**Code Quality**: We verify code follows existing patterns. Consistency reduces cognitive load. When every module works similarly, developers understand new code quickly.
**Test Coverage**: Not every line needs testing, but every behavior does. Tests document intent and protect against regressions.
**Breaking Changes**: May be acceptable in minor versions but must be clearly documented. See the [versioning policy](/development/releases#versioning-policy).
## Special Modules
**`contrib`**: Community-maintained patterns and utilities. Original authors maintain their contributions. Not representative of the core framework.
**`experimental`**: Maintainer-developed features that may preview future functionality. Can break or be deleted at any time without notice. Pin your FastMCP version when using these features.

View file

@ -0,0 +1,79 @@
---
title: "Releases"
description: "FastMCP versioning and release process"
icon: "truck-fast"
---
FastMCP releases frequently to deliver features quickly in the rapidly evolving MCP ecosystem. We use semantic versioning pragmatically - the Model Context Protocol is young, patterns are still emerging, and waiting for perfect stability would mean missing opportunities to empower developers with better tools.
## Versioning Policy
### Semantic Versioning
**Major (x.0.0)**: Complete API redesigns
Major versions represent fundamental shifts. FastMCP 2.x is entirely different from 1.x in both implementation and design philosophy.
**Minor (2.x.0)**: New features and evolution
<Warning>
Unlike traditional semantic versioning, minor versions **may** include [breaking changes](#breaking-changes) when necessary for the ecosystem's evolution. This flexibility is essential in a young ecosystem where perfect backwards compatibility would prevent important improvements.
</Warning>
FastMCP always targets the most current MCP Protocol version. Breaking changes in the MCP spec or MCP SDK automatically flow through to FastMCP - we prioritize staying current with the latest features and conventions over maintaining compatibility with older protocol versions.
**Patch (2.0.x)**: Bug fixes and refinements
Patch versions contain only bug fixes without breaking changes. These are safe updates you can apply with confidence.
### Breaking Changes
We permit breaking changes in minor versions because the MCP ecosystem is rapidly evolving. Refusing to break problematic APIs would accumulate design debt that eventually makes the framework unusable. Each breaking change represents a deliberate decision to keep FastMCP aligned with the ecosystem's evolution.
When breaking changes occur:
- They only happen in minor versions (e.g., 2.3.x to 2.4.0)
- Release notes explain what changed and how to migrate
- We provide deprecation warnings at least 1 minor version in advance when possible
- Changes must substantially benefit users to justify disruption
The public API is what's covered by our compatibility guarantees - these are the parts of FastMCP you can rely on to remain stable within a minor version. The public API consists of:
- `FastMCP` server class, `Client` class, and FastMCP `Context`
- Core MCP components: `Tool`, `Prompt`, `Resource`, `ResourceTemplate`, and transports
- Their public methods and documented behaviors
Everything else (utilities, private methods, internal modules) may change without notice. This boundary lets us refactor internals and improve implementation details without breaking your code. For production stability, pin to specific versions.
<Warning>
The `fastmcp.server.auth` module was introduced in 2.12.0 and is exempted from this policy temporarily, meaning it is *expected* to have breaking changes even on patch versions. This is because auth is a rapidly evolving part of the MCP spec and it would be dangerous to be beholden to old decisions. Please pin your FastMCP version if using authentication in production.
We expect this exemption to last through at least the 2.12.x and 2.13.x release series.
</Warning>
### Production Use
Pin to exact versions:
```
fastmcp==2.11.0 # Good
fastmcp>=2.11.0 # Bad - will install breaking changes
```
## Creating Releases
Our release process is intentionally simple:
1. Create GitHub release with tag `vMAJOR.MINOR.PATCH` (e.g., `v2.11.0`)
2. Generate release notes automatically, and curate or add additional editorial information as needed
3. GitHub releases automatically trigger PyPI deployments
This automation lets maintainers focus on code quality rather than release mechanics.
### Release Cadence
We follow a feature-driven release cadence rather than a fixed schedule. Minor versions ship approximately every 3-4 weeks when significant functionality is ready.
Patch releases ship promptly for:
- Critical bug fixes
- Security updates (immediate release)
- Regression fixes
This approach means you get improvements as soon as they're ready rather than waiting for arbitrary release dates.

350
docs/development/tests.mdx Normal file
View file

@ -0,0 +1,350 @@
---
title: "Tests"
description: "Testing patterns and requirements for FastMCP"
icon: vial
---
Good tests are the foundation of reliable software. In FastMCP, we treat tests as first-class documentation that demonstrates how features work while protecting against regressions. Every new capability needs comprehensive tests that demonstrate correctness.
## FastMCP Tests
### Running Tests
```bash
# Run all tests
uv run pytest
# Run specific test file
uv run pytest tests/server/test_auth.py
# Run with coverage
uv run pytest --cov=fastmcp
# Skip integration tests for faster runs
uv run pytest -m "not integration"
# Skip tests that spawn processes
uv run pytest -m "not integration and not client_process"
```
Tests should complete in under 1 second unless marked as integration tests. This speed encourages running them frequently, catching issues early.
### Test Organization
Our test organization mirrors the `src/` directory structure, creating a predictable mapping between code and tests. When you're working on `src/fastmcp/server/auth.py`, you'll find its tests in `tests/server/test_auth.py`. In rare cases tests are split further - for example, the OpenAPI tests are so comprehensive they're split across multiple files.
### Test Markers
We use pytest markers to categorize tests that require special resources or take longer to run:
```python
@pytest.mark.integration
async def test_github_api_integration():
"""Test GitHub API integration with real service."""
token = os.getenv("FASTMCP_GITHUB_TOKEN")
if not token:
pytest.skip("FASTMCP_GITHUB_TOKEN not available")
# Test against real GitHub API
client = GitHubClient(token)
repos = await client.list_repos("jlowin")
assert "fastmcp" in [repo.name for repo in repos]
@pytest.mark.client_process
async def test_stdio_transport():
"""Test STDIO transport with separate process."""
# This spawns a subprocess
async with Client("python examples/simple_echo.py") as client:
result = await client.call_tool("echo", {"message": "test"})
assert result.content[0].text == "test"
```
## Writing Tests
### Test Requirements
Following these practices creates maintainable, debuggable test suites that serve as both documentation and regression protection.
#### Single Behavior Per Test
Each test should verify exactly one behavior. When it fails, you need to know immediately what broke. A test that checks five things gives you five potential failure points to investigate. A test that checks one thing points directly to the problem.
<CodeGroup>
```python Good: Atomic Test
async def test_tool_registration():
"""Test that tools are properly registered with the server."""
mcp = FastMCP("test-server")
@mcp.tool
def add(a: int, b: int) -> int:
return a + b
tools = mcp.list_tools()
assert len(tools) == 1
assert tools[0].name == "add"
```
```python Bad: Multi-Behavior Test
async def test_server_functionality():
"""Test multiple server features at once."""
mcp = FastMCP("test-server")
# Tool registration
@mcp.tool
def add(a: int, b: int) -> int:
return a + b
# Resource creation
@mcp.resource("config://app")
def get_config():
return {"version": "1.0"}
# Authentication setup
mcp.auth = BearerTokenProvider({"token": "user"})
# What exactly are we testing? If this fails, what broke?
assert mcp.list_tools()
assert mcp.list_resources()
assert mcp.auth is not None
```
</CodeGroup>
#### Self-Contained Setup
Every test must create its own setup. Tests should be runnable in any order, in parallel, or in isolation. When a test fails, you should be able to run just that test to reproduce the issue.
<CodeGroup>
```python Good: Self-Contained
async def test_tool_execution_with_error():
"""Test that tool errors are properly handled."""
mcp = FastMCP("test-server")
@mcp.tool
def divide(a: int, b: int) -> float:
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
async with Client(mcp) as client:
with pytest.raises(Exception):
await client.call_tool("divide", {"a": 10, "b": 0})
```
```python Bad: Test Dependencies
# Global state that tests depend on
test_server = None
def test_setup_server():
"""Setup for other tests."""
global test_server
test_server = FastMCP("shared-server")
def test_server_works():
"""Test server functionality."""
# Depends on test_setup_server running first
assert test_server is not None
```
</CodeGroup>
#### Clear Intent
Test names and assertions should make the verified behavior obvious. A developer reading your test should understand what feature it validates and how that feature should behave.
```python
async def test_authenticated_tool_requires_valid_token():
"""Test that authenticated users can access protected tools."""
mcp = FastMCP("test-server")
mcp.auth = BearerTokenProvider({"secret-token": "test-user"})
@mcp.tool
def protected_action() -> str:
return "success"
async with Client(mcp, auth=BearerAuth("secret-token")) as client:
result = await client.call_tool("protected_action", {})
assert result.content[0].text == "success"
```
#### Using Fixtures
Use fixtures to create reusable data, server configurations, or other resources for your tests. Note that you should **not** open FastMCP clients in your fixtures as it can create hard-to-diagnose issues with event loops.
```python
import pytest
from fastmcp import FastMCP, Client
@pytest.fixture
def weather_server():
server = FastMCP("WeatherServer")
@server.tool
def get_temperature(city: str) -> dict:
temps = {"NYC": 72, "LA": 85, "Chicago": 68}
return {"city": city, "temp": temps.get(city, 70)}
return server
async def test_temperature_tool(weather_server):
async with Client(weather_server) as client:
result = await client.call_tool("get_temperature", {"city": "LA"})
assert result.data == {"city": "LA", "temp": 85}
```
#### Effective Assertions
Assertions should be specific and provide context on failure. When a test fails during CI, the assertion message should tell you exactly what went wrong.
```python
# Basic assertion - minimal context on failure
assert result.status == "success"
# Better - explains what was expected
assert result.status == "success", f"Expected successful operation, got {result.status}: {result.error}"
```
Try not to have too many assertions in a single test unless you truly need to check various aspects of the same behavior. In general, assertions of different behaviors should be in separate tests.
#### Inline Snapshots
FastMCP uses `inline-snapshot` for testing complex data structures. On first run with empty `snapshot()`, pytest will auto-populate the expected value. To update snapshots after intentional changes, run `pytest --inline-snapshot=fix`. This is particularly useful for testing JSON schemas and API responses.
```python
from inline_snapshot import snapshot
async def test_tool_schema_generation():
"""Test that tool schemas are generated correctly."""
mcp = FastMCP("test-server")
@mcp.tool
def calculate_tax(amount: float, rate: float = 0.1) -> dict:
"""Calculate tax on an amount."""
return {"amount": amount, "tax": amount * rate, "total": amount * (1 + rate)}
tools = mcp.list_tools()
schema = tools[0].inputSchema
# First run: snapshot() is empty, gets auto-populated
# Subsequent runs: compares against stored snapshot
assert schema == snapshot({
"type": "object",
"properties": {
"amount": {"type": "number"},
"rate": {"type": "number", "default": 0.1}
},
"required": ["amount"]
})
```
### In-Memory Testing
FastMCP uses in-memory transport for testing, where servers and clients communicate directly. The majority of functionality can be tested in a deterministic fashion this way. We use more complex setups only when testing transports themselves.
The in-memory transport runs the real MCP protocol implementation without network overhead. Instead of deploying your server or managing network connections, you pass your server instance directly to the client. Everything runs in the same Python process - you can set breakpoints anywhere and step through with your debugger.
```python
from fastmcp import FastMCP, Client
# Create your server
server = FastMCP("WeatherServer")
@server.tool
def get_temperature(city: str) -> dict:
"""Get current temperature for a city"""
temps = {"NYC": 72, "LA": 85, "Chicago": 68}
return {"city": city, "temp": temps.get(city, 70)}
async def test_weather_operations():
# Pass server directly - no deployment needed
async with Client(server) as client:
result = await client.call_tool("get_temperature", {"city": "NYC"})
assert result.data == {"city": "NYC", "temp": 72}
```
This pattern makes tests deterministic and fast - typically completing in milliseconds rather than seconds.
### Mocking External Dependencies
FastMCP servers are standard Python objects, so you can mock external dependencies using your preferred approach:
```python
from unittest.mock import AsyncMock
async def test_database_tool():
server = FastMCP("DataServer")
# Mock the database
mock_db = AsyncMock()
mock_db.fetch_users.return_value = [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
]
@server.tool
async def list_users() -> list:
return await mock_db.fetch_users()
async with Client(server) as client:
result = await client.call_tool("list_users", {})
assert len(result.data) == 2
assert result.data[0]["name"] == "Alice"
mock_db.fetch_users.assert_called_once()
```
### Testing Network Transports
While in-memory testing covers most unit testing needs, you'll occasionally need to test actual network transports. Use the `run_server_in_process` utility to spawn a server in a separate process for testing:
```python
import pytest
from fastmcp.utilities.tests import run_server_in_process
from fastmcp import FastMCP, Client
from fastmcp.client.transports import StreamableHttpTransport
def run_server(host: str, port: int) -> None:
"""Function to run in subprocess."""
server = FastMCP("TestServer")
@server.tool
def greet(name: str) -> str:
return f"Hello, {name}!"
server.run(host=host, port=port)
@pytest.fixture
async def http_server():
"""Fixture that runs server in subprocess."""
with run_server_in_process(run_server, transport="http") as url:
yield f"{url}/mcp"
async def test_http_transport(http_server: str):
"""Test actual HTTP transport behavior."""
async with Client(
transport=StreamableHttpTransport(http_server)
) as client:
result = await client.ping()
assert result is True
greeting = await client.call_tool("greet", {"name": "World"})
assert greeting.data == "Hello, World!"
```
The `run_server_in_process` utility handles server lifecycle, port allocation, and cleanup automatically. This pattern is essential for testing transport-specific behavior like timeouts, headers, and authentication. Note that FastMCP often uses the `client_process` marker to isolate tests that spawn processes, as they can create contention in CI.
### Documentation Testing
Documentation requires the same validation as code. The `just docs` command launches a local Mintlify server that renders your documentation exactly as users will see it:
```bash
# Start local documentation server with hot reload
just docs
# Or run Mintlify directly
mintlify dev
```
The local server watches for changes and automatically refreshes. This preview catches formatting issues and helps you see documentation as users will experience it.

View file

@ -50,185 +50,187 @@
"navigation": {
"tabs": [
{
"anchors": [
"groups": [
{
"anchor": "Documentation",
"groups": [
{
"group": "Get Started",
"pages": [
"getting-started/welcome",
"getting-started/installation",
"getting-started/quickstart"
]
},
{
"group": "Servers",
"pages": [
"servers/server",
{
"group": "Core Components",
"icon": "toolbox",
"pages": [
"servers/tools",
"servers/resources",
"servers/prompts"
]
},
{
"group": "Advanced Features",
"icon": "stars",
"pages": [
"servers/context",
"servers/proxy",
"servers/composition",
"servers/elicitation",
"servers/logging",
"servers/progress",
"servers/sampling",
"servers/middleware"
]
},
{
"group": "Authentication",
"icon": "shield-check",
"pages": [
"servers/auth/authentication",
"servers/auth/token-verification",
"servers/auth/remote-oauth",
"servers/auth/oauth-proxy",
"servers/auth/full-oauth-server"
]
},
{
"group": "Deployment",
"icon": "rocket",
"pages": [
"deployment/running-server",
"deployment/server-configuration",
"deployment/testing",
"deployment/self-hosted",
"deployment/fastmcp-cloud"
]
}
]
},
{
"group": "Clients",
"pages": [
{
"group": "Essentials",
"icon": "cube",
"pages": ["clients/client", "clients/transports"]
},
{
"group": "Core Operations",
"icon": "handshake",
"pages": [
"clients/tools",
"clients/resources",
"clients/prompts"
]
},
{
"group": "Advanced Features",
"icon": "stars",
"pages": [
"clients/elicitation",
"clients/logging",
"clients/progress",
"clients/sampling",
"clients/messages",
"clients/roots"
]
},
{
"group": "Authentication",
"icon": "user-shield",
"pages": ["clients/auth/oauth", "clients/auth/bearer"]
}
]
},
{
"group": "Integrations",
"pages": [
{
"group": "Authentication",
"icon": "key",
"pages": [
"integrations/authkit",
"integrations/azure",
"integrations/github",
"integrations/google",
"integrations/workos-oauth"
]
},
{
"group": "Authorization",
"icon": "shield-check",
"pages": [
"integrations/eunomia-authorization",
"integrations/permit"
]
},
{
"group": "AI Assistants",
"icon": "robot",
"pages": [
"integrations/chatgpt",
"integrations/claude-code",
"integrations/claude-desktop",
"integrations/cursor",
"integrations/mcp-json-configuration"
]
},
{
"group": "AI SDKs",
"icon": "code",
"pages": [
"integrations/anthropic",
"integrations/gemini",
"integrations/openai"
]
},
{
"group": "Web Frameworks",
"icon": "globe",
"pages": [
"integrations/asgi",
"integrations/fastapi",
"integrations/openapi"
]
}
]
},
{
"group": "Patterns",
"pages": [
"patterns/tool-transformation",
"patterns/decorating-methods",
"patterns/cli",
"patterns/contrib"
]
},
{
"group": "Tutorials",
"pages": [
"tutorials/mcp",
"tutorials/create-mcp-server",
"tutorials/rest-api"
]
}
],
"icon": "book"
"group": "Get Started",
"pages": [
"getting-started/welcome",
"getting-started/installation",
"getting-started/quickstart"
]
},
{
"anchor": "What's New",
"pages": ["updates", "changelog"]
"group": "Servers",
"pages": [
"servers/server",
{
"group": "Core Components",
"icon": "toolbox",
"pages": [
"servers/tools",
"servers/resources",
"servers/prompts"
]
},
{
"group": "Advanced Features",
"icon": "stars",
"pages": [
"servers/context",
"servers/proxy",
"servers/composition",
"servers/elicitation",
"servers/logging",
"servers/progress",
"servers/sampling",
"servers/middleware"
]
},
{
"group": "Authentication",
"icon": "shield-check",
"pages": [
"servers/auth/authentication",
"servers/auth/token-verification",
"servers/auth/remote-oauth",
"servers/auth/oauth-proxy",
"servers/auth/full-oauth-server"
]
},
{
"group": "Deployment",
"icon": "rocket",
"pages": [
"deployment/running-server",
"deployment/server-configuration",
"deployment/self-hosted",
"deployment/fastmcp-cloud"
]
}
]
},
{
"group": "Clients",
"pages": [
{
"group": "Essentials",
"icon": "cube",
"pages": [
"clients/client",
"clients/transports"
]
},
{
"group": "Core Operations",
"icon": "handshake",
"pages": [
"clients/tools",
"clients/resources",
"clients/prompts"
]
},
{
"group": "Advanced Features",
"icon": "stars",
"pages": [
"clients/elicitation",
"clients/logging",
"clients/progress",
"clients/sampling",
"clients/messages",
"clients/roots"
]
},
{
"group": "Authentication",
"icon": "user-shield",
"pages": [
"clients/auth/oauth",
"clients/auth/bearer"
]
}
]
},
{
"group": "Integrations",
"pages": [
{
"group": "Authentication",
"icon": "key",
"pages": [
"integrations/authkit",
"integrations/azure",
"integrations/github",
"integrations/google",
"integrations/workos-oauth"
]
},
{
"group": "Authorization",
"icon": "shield-check",
"pages": [
"integrations/eunomia-authorization",
"integrations/permit"
]
},
{
"group": "AI Assistants",
"icon": "robot",
"pages": [
"integrations/chatgpt",
"integrations/claude-code",
"integrations/claude-desktop",
"integrations/cursor",
"integrations/mcp-json-configuration"
]
},
{
"group": "AI SDKs",
"icon": "code",
"pages": [
"integrations/anthropic",
"integrations/gemini",
"integrations/openai"
]
},
{
"group": "Web Frameworks",
"icon": "globe",
"pages": [
"integrations/asgi",
"integrations/fastapi",
"integrations/openapi"
]
}
]
},
{
"group": "Patterns",
"pages": [
"patterns/tool-transformation",
"patterns/decorating-methods",
"patterns/cli",
"patterns/contrib"
]
},
{
"group": "Development",
"pages": [
"development/contributing",
"development/tests",
"development/releases"
]
}
],
"tab": "Documentation"
},
{
"pages": [
"updates",
"changelog"
],
"tab": "What's New"
},
{
"anchors": [
{

View file

@ -61,50 +61,22 @@ mcp = FastMCP("My MCP Server")
Prior to `fastmcp==2.3.0` and `mcp==1.8.0`, the 2.x API always mirrored the official 1.0 API. However, as the projects diverge, this can not be guaranteed. You may see deprecation warnings if you attempt to use 1.0 APIs in FastMCP 2.x. Please refer to this documentation for details on new capabilities.
</Warning>
## Versioning and Breaking Changes
## Versioning Policy
While we make every effort not to introduce backwards-incompatible changes to our public APIs and behavior, FastMCP exists in a rapidly evolving MCP landscape. We're committed to bringing the most cutting-edge features to our users, which occasionally necessitates changes to existing functionality.
FastMCP follows semantic versioning with pragmatic adaptations for the rapidly evolving MCP ecosystem. Breaking changes may occur in minor versions (e.g., 2.3.x to 2.4.0) when necessary to stay current with the MCP Protocol.
As a practice, breaking changes will only occur on minor version changes (e.g., 2.3.x to 2.4.0). A minor version change indicates either:
- A significant new feature set that warrants a new minor version
- Introducing breaking changes that may affect behavior on upgrade
For users concerned about stability in production environments, we recommend pinning FastMCP to a specific version in your dependencies.
Whenever possible, FastMCP will issue deprecation warnings when users attempt to use APIs that are either deprecated or destined for future removal. These warnings will be maintained for at least 1 minor version release, and may be maintained longer.
Note that the "public API" includes the public functionality of the `FastMCP` server, core FastMCP components like `Tool`, `Prompt`, `Resource`, and `ResourceTemplate`, and their respective public methods. It does not include private methods, utilities, or objects that are stored as private attributes, as we do not expect users to rely on those implementation details.
## Installing for Development
If you plan to contribute to FastMCP, you should begin by cloning the repository and using uv to install all dependencies (development dependencies are installed automatically):
```bash
git clone https://github.com/jlowin/fastmcp.git
cd fastmcp
uv sync
For production use, always pin to exact versions:
```
fastmcp==2.11.0 # Good
fastmcp>=2.11.0 # Bad - will install breaking changes
```
This will install all dependencies, including ones for development, and create a virtual environment, which you can activate and use as normal.
See the full [versioning and release policy](/development/releases#versioning-policy) for details on our public API, deprecation practices, and breaking change philosophy.
### Unit Tests
## Contributing to FastMCP
FastMCP has a comprehensive unit test suite, and all PR's must introduce and pass appropriate tests. To run the tests, use pytest:
```bash
pytest
```
### Pre-Commit Hooks
FastMCP uses pre-commit to manage code quality, including formatting, linting, and type-safety. All PRs must pass the pre-commit hooks, which are run as a part of the CI process. To install the pre-commit hooks, run:
```bash
uv run pre-commit install
```
Alternatively, to run pre-commit manually at any time, use:
```bash
pre-commit run --all-files
```
Interested in contributing to FastMCP? See the [Contributing Guide](/development/contributing) for details on:
- Setting up your development environment
- Running tests and pre-commit hooks
- Submitting issues and pull requests
- Code standards and review process

View file

@ -7,13 +7,13 @@ sidebarTitle: oauth
## Functions
### `default_cache_dir` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L48" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `default_cache_dir` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L55" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
default_cache_dir() -> Path
```
### `check_if_auth_required` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L192" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `check_if_auth_required` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L199" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
check_if_auth_required(mcp_url: str, httpx_kwargs: dict[str, Any] | None = None) -> bool
@ -28,13 +28,19 @@ Check if the MCP endpoint requires authentication by making a test request.
## Classes
### `StoredToken` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L37" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ClientNotFoundError` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L38" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Raised when OAuth client credentials are not found on the server.
### `StoredToken` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L44" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Token storage format with absolute expiry time.
### `FileTokenStorage` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L52" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `FileTokenStorage` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L59" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
File-based token storage implementation for OAuth credentials and tokens.
@ -45,7 +51,7 @@ Each instance is tied to a specific server URL for proper token isolation.
**Methods:**
#### `get_base_url` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_base_url` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L74" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_base_url(url: str) -> str
@ -54,7 +60,7 @@ get_base_url(url: str) -> str
Extract the base URL (scheme + host) from a URL.
#### `get_cache_key` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L72" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_cache_key` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L79" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_cache_key(self) -> str
@ -63,7 +69,7 @@ get_cache_key(self) -> str
Generate a safe filesystem key from the server's base URL.
#### `get_tokens` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L87" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tokens` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L94" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tokens(self) -> OAuthToken | None
@ -72,7 +78,7 @@ get_tokens(self) -> OAuthToken | None
Load tokens from file storage.
#### `set_tokens` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L119" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `set_tokens` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L126" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_tokens(self, tokens: OAuthToken) -> None
@ -81,7 +87,7 @@ set_tokens(self, tokens: OAuthToken) -> None
Save tokens to file storage.
#### `get_client_info` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L136" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_client_info` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L143" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_client_info(self) -> OAuthClientInformationFull | None
@ -90,7 +96,7 @@ get_client_info(self) -> OAuthClientInformationFull | None
Load client information from file storage.
#### `set_client_info` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L164" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `set_client_info` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L171" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_client_info(self, client_info: OAuthClientInformationFull) -> None
@ -99,7 +105,7 @@ set_client_info(self, client_info: OAuthClientInformationFull) -> None
Save client information to file storage.
#### `clear` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L170" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `clear` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L177" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
clear(self) -> None
@ -108,7 +114,7 @@ clear(self) -> None
Clear all cached data for this server.
#### `clear_all` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L179" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `clear_all` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L186" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
clear_all(cls, cache_dir: Path | None = None) -> None
@ -117,7 +123,7 @@ clear_all(cls, cache_dir: Path | None = None) -> None
Clear all cached data for all servers.
### `OAuth` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L222" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `OAuth` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L229" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
OAuth client provider for MCP servers with browser-based authentication.
@ -128,16 +134,16 @@ a browser for user authorization and running a local callback server.
**Methods:**
#### `redirect_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L302" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `redirect_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L309" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
redirect_handler(self, authorization_url: str) -> None
```
Open browser for authorization.
Open browser for authorization, with pre-flight check for invalid client.
#### `callback_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L307" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `callback_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L330" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
callback_handler(self) -> tuple[str, str | None]
@ -145,3 +151,15 @@ callback_handler(self) -> tuple[str, str | None]
Handle OAuth callback and return (auth_code, state).
#### `async_auth_flow` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L363" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]
```
HTTPX auth flow with automatic retry on stale cached credentials.
If the OAuth flow fails due to invalid/stale client credentials,
clears the cache and retries once with fresh registration.

View file

@ -127,7 +127,6 @@ OAuth Flow Implementation
1. Client Registration (DCR):
- Accept any client registration request
- Store ProxyDCRClient that accepts dynamic redirect URIs
- Return shared upstream credentials to all clients
2. Authorization:
- Store transaction mapping client details to proxy flow
@ -182,51 +181,33 @@ Handles provider-specific requirements:
**Methods:**
#### `get_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L325" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L330" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_client(self, client_id: str) -> OAuthClientInformationFull | None
```
Get client information by ID.
Get client information by ID. This is generally the random ID
provided to the DCR client during registration, not the upstream client ID.
For unregistered clients, returns a ProxyDCRClient that accepts
any localhost redirect URI for DCR clients.
Even registered clients use ProxyDCRClient to ensure they can
authenticate with different dynamic ports on reconnection. This
handles the case where a client with cached tokens reconnects
on a different port.
For unregistered clients, returns None (which will raise an error in the SDK).
#### `register_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L358" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `register_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L340" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
register_client(self, client_info: OAuthClientInformationFull) -> None
```
Register a client locally using fixed upstream credentials.
Register a client locally
This implementation always uses the upstream client_id and client_secret
regardless of what the client requests. It modifies the client_info object
in place since the MCP framework ignores return values.
This ensures all clients use the same credentials that are registered
with the upstream server.
Implementation Detail:
We store a ProxyDCRClient (not the original client_info) to ensure
the client can reconnect with different dynamic redirect URIs. This is
essential for cached token scenarios where the client port changes.
The flow:
1. Client provides its desired redirect URIs (dynamic localhost ports)
2. We create a ProxyDCRClient that will accept ANY localhost URI
3. We store this flexible client for future authentications
4. When client reconnects with a different port, ProxyDCRClient accepts it
When a client registers, we create a ProxyDCRClient that is more
forgiving about validating redirect URIs, since the DCR client's
redirect URI will likely be localhost or unknown to the proxied IDP. The
proxied IDP only knows about this server's fixed redirect URI.
#### `authorize` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L417" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `authorize` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L383" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
@ -240,7 +221,7 @@ This implements the DCR-compliant proxy pattern:
3. Redirect to IdP with our fixed callback URL
#### `load_authorization_code` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L480" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `load_authorization_code` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L439" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None
@ -252,7 +233,7 @@ Look up our client code and return authorization code object
with PKCE challenge for validation.
#### `exchange_authorization_code` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L522" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `exchange_authorization_code` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L481" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken
@ -264,7 +245,7 @@ For the DCR-compliant proxy flow, we return the IdP tokens that were obtained
during the IdP callback exchange. PKCE validation is handled by the MCP framework.
#### `load_refresh_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L589" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `load_refresh_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L548" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None
@ -273,7 +254,7 @@ load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str)
Load refresh token from local storage.
#### `exchange_refresh_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L597" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `exchange_refresh_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L556" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken
@ -282,7 +263,7 @@ exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token:
Exchange refresh token for new access token using authlib.
#### `load_access_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L672" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `load_access_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L631" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
load_access_token(self, token: str) -> AccessToken | None
@ -294,7 +275,7 @@ Delegates to the JWT verifier which handles signature validation,
expiration checking, and claims validation using the upstream JWKS.
#### `revoke_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L689" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `revoke_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L648" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
revoke_token(self, token: AccessToken | RefreshToken) -> None
@ -306,7 +287,7 @@ Removes tokens from local storage and attempts to revoke them with
the upstream server if a revocation endpoint is configured.
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L733" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L692" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_routes(self, mcp_path: str | None = None, mcp_endpoint: Any | None = None) -> list[Route]

View file

@ -82,7 +82,7 @@ validate_source(cls, v: dict | Source) -> SourceType
Validate and convert source to proper format.
Supports:
- Dict format: {"path": "server.py", "entrypoint": "app"}
- Dict format: `{"path": "server.py", "entrypoint": "app"}`
- FileSystemSource instance (passed through)
No string parsing happens here - that's only at CLI boundaries.

View file

@ -184,7 +184,7 @@ class MCPServerConfig(BaseModel):
"""Validate and convert source to proper format.
Supports:
- Dict format: {"path": "server.py", "entrypoint": "app"}
- Dict format: `{"path": "server.py", "entrypoint": "app"}`
- FileSystemSource instance (passed through)
No string parsing happens here - that's only at CLI boundaries.