Merge branch 'main' into main

This commit is contained in:
William Easton 2025-08-25 10:06:59 -05:00 committed by GitHub
commit 1a72bfa4a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
119 changed files with 5776 additions and 2547 deletions

28
.github/pull_request_template.md vendored Normal file
View file

@ -0,0 +1,28 @@
## Description
<!--
Please provide a clear and concise description of the changes made in this pull request.
Using AI to generate code? Please include a note in the description with which AI tool you used.
-->
**Contributors Checklist**
<!--
NOTE:
1. You must create an issue in the repository before making a Pull Request.
2. You must not create a Pull Request for an issue that is already assigned to someone else.
If you do not follow these steps, your Pull Request will be closed without review.
-->
- [ ] My change is related to issue #(issue number)
- [ ] I have followed the repository's development workflow
- [ ] I have tested my changes manually and by adding relevant tests
- [ ] I have performed all required documentation updates
**Review Checklist**
<!-- Your Pull Request will not be reviewed if tests are failing, you have not self-reviewed your changes, or you have not checked all of the following: -->
- [ ] I have self-reviewed my changes
- [ ] My Pull Request is ready for review
---

View file

@ -15,10 +15,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@v6
- name: Auto-close duplicate issues
run: uv run scripts/auto_close_duplicates.py

View file

@ -20,7 +20,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Generate Marvin App token
id: marvin-token
@ -45,7 +45,7 @@ jobs:
4. Next, feed the results from steps 2 and 3 into another agent using the Task tool, so that it can filter out false positives that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed.
5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates)
5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates). If there are no duplicates, DO NOT COMMENT. Just exit.
Notes for your agents:
- Use `gh` to interact with GitHub, rather than web fetch
@ -72,8 +72,9 @@ jobs:
- name: Run Marvin dedupe command
uses: anthropics/claude-code-base-action@beta
with:
model: claude-3-5-haiku-latest
prompt_file: /tmp/claude-prompts/dedupe-prompt.txt
allowed_tools: "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh api:*),Bash(gh issue comment:*),Task"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }}
claude_env: |
GH_TOKEN: ${{ steps.marvin-token.outputs.token }}

View file

@ -27,7 +27,7 @@ jobs:
steps:
- name: Checkout base repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
repository: ${{ github.repository }}
ref: ${{ github.event.repository.default_branch }}
@ -151,7 +151,7 @@ jobs:
prompt_file: /tmp/claude-prompts/triage-prompt.txt
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__get_pull_request_files"
timeout_minutes: "5"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }}
mcp_config: /tmp/mcp-config/mcp-servers.json
claude_env: |
GH_TOKEN: ${{ steps.marvin-token.outputs.token }}

View file

@ -31,24 +31,18 @@ jobs:
(github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'marvin')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Set up Python environment
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: actions/checkout@v5
# Install UV package manager
- name: Install UV
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
# Install project dependencies
- name: Install dependencies
run: uv sync --dev
run: uv sync --python 3.12
# Install pre-commit hooks automatically
- name: Install pre-commit hooks

View file

@ -12,7 +12,7 @@ jobs:
id-token: write # For PyPI's trusted publishing
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0

View file

@ -30,7 +30,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v6
with:

View file

@ -34,7 +34,7 @@ jobs:
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v6
@ -48,10 +48,10 @@ jobs:
run: uv sync --frozen
- name: Run tests (excluding integration and client_process)
run: uv run pytest tests -m "not integration and not client_process"
run: uv run pytest -v tests -m "not integration and not client_process"
- name: Run client process tests separately
run: uv run pytest tests -m "client_process" -x
run: uv run pytest -v tests -m "client_process" -x
run_integration_tests:
name: "Run integration tests"
@ -59,7 +59,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v6
@ -73,7 +73,7 @@ jobs:
run: uv sync --frozen
- name: Run integration tests
run: uv run pytest tests -m "integration"
run: uv run pytest -v tests -m "integration"
env:
FASTMCP_GITHUB_TOKEN: ${{ secrets.FASTMCP_GITHUB_TOKEN }}
FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID }}

View file

@ -0,0 +1,89 @@
name: Update FastMCPConfig Schema
# This workflow runs on merges to main to automatically update the config schema
# by creating a PR when changes are needed.
on:
push:
branches: ["main"]
paths:
- "src/fastmcp/utilities/fastmcp_config/**"
- "!src/fastmcp/utilities/fastmcp_config/v1/schema.json" # Exclude the local schema file
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
update-config-schema:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install dependencies
run: uv sync --python 3.12
- name: Generate config schema
run: |
echo "🔄 Generating fastmcp.json schema..."
# Generate schema in docs/public for web access
uv run python -c "
from fastmcp.utilities.fastmcp_config import generate_schema
generate_schema('docs/public/schemas/fastmcp.json/latest.json')
print('✅ Latest schema generated in docs/public')
"
# Also update the v1 schema in docs/public
uv run python -c "
from fastmcp.utilities.fastmcp_config import generate_schema
generate_schema('docs/public/schemas/fastmcp.json/v1.json')
print('✅ v1 schema generated in docs/public')
"
# Generate schema in the source directory for local development
uv run python -c "
from fastmcp.utilities.fastmcp_config import generate_schema
generate_schema('src/fastmcp/utilities/fastmcp_config/v1/schema.json')
print('✅ Schema generated in utilities/fastmcp_config/v1/')
"
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ steps.marvin-token.outputs.token }}
commit-message: "chore: Update fastmcp.json schema"
title: "chore: Update fastmcp.json schema"
body: |
This PR updates the fastmcp.json schema files to match the current source code.
The schema is automatically generated from `src/fastmcp/utilities/fastmcp_config/` to ensure consistency.
**Note:** This PR is fully automated and will update itself with any subsequent changes to the schema, or close automatically if the schema becomes up-to-date through other means. Feel free to leave it open until you're ready to merge.
🤖 Generated by Marvin
branch: marvin/update-config-schema
delete-branch: true
author: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
committer: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
- name: Summary
run: |
echo "✅ Config schema generation workflow completed"
echo "PR will be created if there are changes, or closed if schema is already up to date"

View file

@ -1,8 +1,7 @@
name: Update SDK Documentation
# This workflow runs on merges to main to automatically update SDK docs
# without blocking PRs. SDK doc generation can fail if another PR merges
# first, so handling it post-merge prevents annoying CI failures for contributors.
# by creating a PR when changes are needed.
on:
push:
@ -22,9 +21,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
- name: Install uv
uses: astral-sh/setup-uv@v6
@ -32,13 +36,8 @@ jobs:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: uv sync --dev
run: uv sync --python 3.12
- name: Install just
uses: extractions/setup-just@v3
@ -48,34 +47,26 @@ jobs:
echo "🔄 Generating SDK documentation..."
just api-ref-all
- name: Check for changes
id: check_changes
run: |
if git diff --quiet docs/python-sdk/ docs/docs.json; then
echo "No changes detected in SDK documentation"
echo "has_changes=false" >> $GITHUB_OUTPUT
else
echo "Changes detected in SDK documentation"
echo "has_changes=true" >> $GITHUB_OUTPUT
fi
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ steps.marvin-token.outputs.token }}
commit-message: "chore: Update SDK documentation"
title: "chore: Update SDK documentation"
body: |
This PR updates the auto-generated SDK documentation to reflect the latest source code changes.
- name: Commit and push changes
if: steps.check_changes.outputs.has_changes == 'true'
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add docs/python-sdk/ docs/docs.json
git commit -m "chore: Update SDK documentation
📚 Documentation is automatically generated from the source code docstrings and type annotations.
🤖 Generated with [Claude Code](https://claude.ai/code)
**Note:** This PR is fully automated and will update itself with any subsequent changes to the SDK, or close automatically if the documentation becomes up-to-date through other means. Feel free to leave it open until you're ready to merge.
Co-Authored-By: Claude <noreply@anthropic.com>"
git push
🤖 Generated by Marvin
branch: marvin/update-sdk-docs
delete-branch: true
author: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
committer: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
- name: Summary
run: |
if [ "${{ steps.check_changes.outputs.has_changes }}" == "true" ]; then
echo "✅ SDK documentation updated and committed"
else
echo "✅ SDK documentation is already up to date"
fi
echo "✅ SDK documentation generation workflow completed"
echo "PR will be created if there are changes, or closed if documentation is already up to date"

6
.gitignore vendored
View file

@ -64,7 +64,13 @@ dmypy.json
# Claude worktree management
.claude-wt/worktrees
# Agents
/PLAN.md
/TODO.md
/STATUS.md
# Common FastMCP test files
/test.py
/server.py
/client.py
/test.json

View file

@ -25,7 +25,7 @@ repos:
- repo: local
hooks:
- id: ty
name: type check
name: ty check
entry: uv run ty check
language: system
types: [python]
@ -37,4 +37,5 @@ repos:
rev: v6.0.0
hooks:
- id: no-commit-to-branch
name: prevent commits to main
args: [--branch, main]

View file

@ -0,0 +1,348 @@
{
"$defs": {
"Deployment": {
"description": "Configuration for server deployment and runtime settings.",
"properties": {
"transport": {
"anyOf": [
{
"enum": [
"stdio",
"http",
"sse"
],
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Transport protocol to use",
"title": "Transport"
},
"host": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Host to bind to when using HTTP transport",
"examples": [
"127.0.0.1",
"0.0.0.0",
"localhost"
],
"title": "Host"
},
"port": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Port to bind to when using HTTP transport",
"examples": [
8000,
3000,
5000
],
"title": "Port"
},
"path": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "URL path for the server endpoint",
"examples": [
"/mcp/",
"/api/mcp/",
"/sse/"
],
"title": "Path"
},
"log_level": {
"anyOf": [
{
"enum": [
"DEBUG",
"INFO",
"WARNING",
"ERROR",
"CRITICAL"
],
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Log level for the server",
"title": "Log Level"
},
"cwd": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Working directory for the server process",
"examples": [
".",
"./src",
"/app"
],
"title": "Cwd"
},
"env": {
"anyOf": [
{
"additionalProperties": {
"type": "string"
},
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "Environment variables to set when running the server",
"examples": [
{
"API_KEY": "secret",
"DEBUG": "true"
}
],
"title": "Env"
},
"args": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Arguments to pass to the server (after --)",
"examples": [
[
"--config",
"config.json",
"--debug"
]
],
"title": "Args"
}
},
"title": "Deployment",
"type": "object"
},
"Environment": {
"description": "Configuration for Python environment setup.",
"properties": {
"python": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Python version constraint",
"examples": [
"3.10",
"3.11",
"3.12"
],
"title": "Python"
},
"dependencies": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Python packages to install with PEP 508 specifiers",
"examples": [
[
"fastmcp>=2.0,<3",
"httpx",
"pandas>=2.0"
]
],
"title": "Dependencies"
},
"requirements": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Path to requirements.txt file",
"examples": [
"requirements.txt",
"../requirements/prod.txt"
],
"title": "Requirements"
},
"project": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Path to project directory containing pyproject.toml",
"examples": [
".",
"../my-project"
],
"title": "Project"
},
"editable": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Directory to install in editable mode",
"examples": [
".",
"../my-package"
],
"title": "Editable"
}
},
"title": "Environment",
"type": "object"
},
"FileSystemSource": {
"description": "Source for local Python files.",
"properties": {
"type": {
"const": "filesystem",
"default": "filesystem",
"description": "Source type",
"title": "Type",
"type": "string"
},
"path": {
"description": "Path to Python file containing the server",
"title": "Path",
"type": "string"
},
"entrypoint": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
"title": "Entrypoint"
}
},
"required": [
"path"
],
"title": "FileSystemSource",
"type": "object"
}
},
"description": "Configuration file for FastMCP servers",
"properties": {
"$schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"description": "JSON schema for IDE support and validation",
"title": "$Schema"
},
"source": {
"$ref": "#/$defs/FileSystemSource",
"description": "Source configuration for the server",
"examples": [
{
"path": "server.py"
},
{
"entrypoint": "app",
"path": "server.py"
},
{
"entrypoint": "mcp",
"path": "src/server.py",
"type": "filesystem"
}
]
},
"environment": {
"$ref": "#/$defs/Environment",
"description": "Python environment setup configuration"
},
"deployment": {
"$ref": "#/$defs/Deployment",
"description": "Server deployment and runtime settings"
}
},
"required": [
"source"
],
"title": "FastMCP Configuration",
"type": "object",
"$id": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json"
}

View file

@ -0,0 +1,348 @@
{
"$defs": {
"Deployment": {
"description": "Configuration for server deployment and runtime settings.",
"properties": {
"transport": {
"anyOf": [
{
"enum": [
"stdio",
"http",
"sse"
],
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Transport protocol to use",
"title": "Transport"
},
"host": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Host to bind to when using HTTP transport",
"examples": [
"127.0.0.1",
"0.0.0.0",
"localhost"
],
"title": "Host"
},
"port": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Port to bind to when using HTTP transport",
"examples": [
8000,
3000,
5000
],
"title": "Port"
},
"path": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "URL path for the server endpoint",
"examples": [
"/mcp/",
"/api/mcp/",
"/sse/"
],
"title": "Path"
},
"log_level": {
"anyOf": [
{
"enum": [
"DEBUG",
"INFO",
"WARNING",
"ERROR",
"CRITICAL"
],
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Log level for the server",
"title": "Log Level"
},
"cwd": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Working directory for the server process",
"examples": [
".",
"./src",
"/app"
],
"title": "Cwd"
},
"env": {
"anyOf": [
{
"additionalProperties": {
"type": "string"
},
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "Environment variables to set when running the server",
"examples": [
{
"API_KEY": "secret",
"DEBUG": "true"
}
],
"title": "Env"
},
"args": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Arguments to pass to the server (after --)",
"examples": [
[
"--config",
"config.json",
"--debug"
]
],
"title": "Args"
}
},
"title": "Deployment",
"type": "object"
},
"Environment": {
"description": "Configuration for Python environment setup.",
"properties": {
"python": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Python version constraint",
"examples": [
"3.10",
"3.11",
"3.12"
],
"title": "Python"
},
"dependencies": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Python packages to install with PEP 508 specifiers",
"examples": [
[
"fastmcp>=2.0,<3",
"httpx",
"pandas>=2.0"
]
],
"title": "Dependencies"
},
"requirements": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Path to requirements.txt file",
"examples": [
"requirements.txt",
"../requirements/prod.txt"
],
"title": "Requirements"
},
"project": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Path to project directory containing pyproject.toml",
"examples": [
".",
"../my-project"
],
"title": "Project"
},
"editable": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Directory to install in editable mode",
"examples": [
".",
"../my-package"
],
"title": "Editable"
}
},
"title": "Environment",
"type": "object"
},
"FileSystemSource": {
"description": "Source for local Python files.",
"properties": {
"type": {
"const": "filesystem",
"default": "filesystem",
"description": "Source type",
"title": "Type",
"type": "string"
},
"path": {
"description": "Path to Python file containing the server",
"title": "Path",
"type": "string"
},
"entrypoint": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
"title": "Entrypoint"
}
},
"required": [
"path"
],
"title": "FileSystemSource",
"type": "object"
}
},
"description": "Configuration file for FastMCP servers",
"properties": {
"$schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"description": "JSON schema for IDE support and validation",
"title": "$Schema"
},
"source": {
"$ref": "#/$defs/FileSystemSource",
"description": "Source configuration for the server",
"examples": [
{
"path": "server.py"
},
{
"entrypoint": "app",
"path": "server.py"
},
{
"entrypoint": "mcp",
"path": "src/server.py",
"type": "filesystem"
}
]
},
"environment": {
"$ref": "#/$defs/Environment",
"description": "Python environment setup configuration"
},
"deployment": {
"$ref": "#/$defs/Deployment",
"description": "Server deployment and runtime settings"
}
},
"required": [
"source"
],
"title": "FastMCP Configuration",
"type": "object",
"$id": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json"
}

View file

@ -1,16 +1,18 @@
---
title: Server Configuration with fastmcp.json
sidebarTitle: Server Configuration
description: Use fastmcp.json for declarative server configuration
title: "Project Configuration"
sidebarTitle: "Project Configuration"
description: Use fastmcp.json for portable, declarative project configuration
icon: file-code
---
import { VersionBadge } from "/snippets/version-badge.mdx"
<VersionBadge version="2.11.4" />
<VersionBadge version="2.12.0" />
FastMCP supports declarative configuration through `fastmcp.json` files. This is the canonical and preferred way to configure FastMCP projects, providing a single source of truth for server settings, dependencies, and deployment options that replaces complex command-line arguments.
The `fastmcp.json` file is designed to be a portable description of your server configuration that can be shared across environments and teams. When running from a `fastmcp.json` file, you can override any configuration values using CLI arguments.
## Overview
The `fastmcp.json` configuration file allows you to define all aspects of your FastMCP server in a structured, shareable format. Instead of remembering command-line arguments or writing shell scripts, you declare your server's configuration once and use it everywhere.
@ -27,89 +29,110 @@ fastmcp run
This configuration approach ensures reproducible deployments across different environments, from local development to production servers. It works seamlessly with Claude Desktop, VS Code extensions, and any MCP-compatible client.
## JSON Schema Support
## File Structure
The `fastmcp.json` configuration answers three fundamental questions about your server:
- **Source** = WHERE does your server code live?
- **Environment** = WHAT environment setup does it require?
- **Deployment** = HOW should the server run?
This conceptual model helps you understand the purpose of each configuration section and organize your settings effectively. The configuration file maps directly to these three concerns:
```json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
// WHERE: Location of your server code
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
// WHAT: Python environment and dependencies
},
"deployment": {
// HOW: Runtime configuration
}
}
```
Only the `source` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed.
### JSON Schema Support
FastMCP provides JSON schemas for IDE autocomplete and validation. Add the schema reference to your `fastmcp.json` for enhanced developer experience:
```json
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "server.py",
"entrypoint": "mcp"
}
}
```
Two schema URLs are available:
- **Version-specific**: `https://gofastmcp.com/schemas/fastmcp_config/v1.json`
- **Latest version**: `https://gofastmcp.com/schemas/fastmcp_config/latest.json`
- **Version-specific**: `https://gofastmcp.com/public/schemas/fastmcp.json/v1.json`
- **Latest version**: `https://gofastmcp.com/public/schemas/fastmcp.json/latest.json`
Modern IDEs like VS Code will automatically provide autocomplete suggestions, validation, and inline documentation when the schema is specified.
## File Structure
### Source Configuration
The `fastmcp.json` file has three main sections, each controlling a different aspect of your server:
The source configuration determines **WHERE** your server code lives. It tells FastMCP how to find and load your server, whether it's a local Python file, a remote repository, or hosted in the cloud. This section is required and forms the foundation of your configuration.
```json
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
},
"environment": {
// Python environment and dependencies
},
"deployment": {
// Runtime configuration
}
}
```
Only the `entrypoint` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed.
## Configuration Fields
### Entrypoint
The entrypoint specifies which Python file and object contains your FastMCP server. This field is required and supports multiple formats to accommodate different project structures.
<Card icon="code" title="Entrypoint Configuration">
<ParamField body="entrypoint" type="object | string" required>
The server entry point. Can be specified in three formats:
<Card icon="code" title="Source">
<ParamField body="source" type="object" required>
The server source configuration that determines where your server code lives.
**Object format** (recommended): Explicit file and object specification
```json
"entrypoint": {
"file": "src/server.py",
"object": "mcp"
}
```
<ParamField body="type" type="string" default="filesystem">
The source type identifier that determines which implementation to use. Currently supports `"filesystem"` for local files. Future releases will add support for `"git"` and `"cloud"` source types.
</ParamField>
**String with object**: File path with colon and object name
```json
"entrypoint": "src/server.py:app"
```
**String format**: Simple path to Python file (searches for common names: mcp, server, app)
```json
"entrypoint": "server.py"
```
<Expandable title="Path Resolution">
- File paths are resolved relative to the configuration file's location
- If your `fastmcp.json` is in a project root and references `src/server.py`, FastMCP will look for the server at `<project_root>/src/server.py`
- When no object is specified, FastMCP automatically searches for common server names: `mcp`, `server`, or `app`
<Expandable title="FileSystemSource">
When `type` is `"filesystem"` (or omitted), the source points to a local Python file containing your FastMCP server:
<ParamField body="path" type="string" required>
Path to the Python file containing your FastMCP server.
</ParamField>
<ParamField body="entrypoint" type="string">
Name of the server instance or factory function within the module:
- Can be a FastMCP server instance (e.g., `mcp = FastMCP("MyServer")`)
- Can be a function with no arguments that returns a FastMCP server
- If not specified, FastMCP searches for common names: `mcp`, `server`, or `app`
</ParamField>
**Example:**
```json
"source": {
"type": "filesystem",
"path": "src/server.py",
"entrypoint": "mcp"
}
```
Note: File paths are resolved relative to the configuration file's location.
</Expandable>
</ParamField>
</Card>
### Environment
<Note>
**Future Source Types**
The environment section configures Python dependencies and version requirements. When specified, FastMCP uses `uv` to create an isolated environment for your server, ensuring reproducible deployments across different systems.
Future releases will support additional source types:
- **Git repositories** (`type: "git"`) for loading server code directly from version control
- **FastMCP Cloud** (`type: "cloud"`) for hosted servers with automatic scaling and management
</Note>
<Card icon="code" title="Environment Configuration">
### Environment Configuration
The environment configuration determines **WHAT** environment setup your server requires. It controls the build-time setup of your Python environment using `uv`'s powerful dependency management. This section ensures your server runs with the exact Python version and dependencies it requires, creating isolated, reproducible environments across different systems.
These settings leverage standard `uv` arguments for environment creation. When any environment field is specified, FastMCP automatically creates an isolated environment before running your server. This build-time configuration happens once when the server starts, not during runtime execution.
<Card icon="code" title="Environment Fields">
<ParamField body="environment" type="object">
Optional Python environment configuration. When any field is specified, FastMCP automatically creates an isolated environment using `uv`.
@ -143,9 +166,9 @@ The environment section configures Python dependencies and version requirements.
</ParamField>
<ParamField body="editable" type="string">
Path to a package to install in editable/development mode.
Path to a package to install in editable/development mode. Useful for local development when you want changes to be reflected immediately.
```json
"editable": "./my-package"
"editable": "."
```
</ParamField>
</Expandable>
@ -157,11 +180,15 @@ When environment configuration is provided, FastMCP:
2. Installs the specified dependencies
3. Runs your server in this clean environment
### Deployment
This build-time setup ensures your server always has the dependencies it needs, without polluting your system Python or conflicting with other projects.
The deployment section controls runtime configuration including transport protocol, networking, logging, and environment variables.
### Deployment Configuration
<Card icon="code" title="Deployment Configuration">
The deployment configuration controls **HOW** your server runs. It defines the runtime behavior including network settings, environment variables, and execution context. These settings determine how your server operates when it executes, from transport protocols to logging levels.
Environment variables are included in this section because they're runtime configuration that affects how your server behaves when it executes, not how its environment is built. The deployment configuration is applied every time your server starts, controlling its operational characteristics.
<Card icon="code" title="Deployment Fields">
<ParamField body="deployment" type="object">
Optional runtime configuration for the server.
@ -220,243 +247,7 @@ The deployment section controls runtime configuration including transport protoc
</ParamField>
</Card>
## Usage with CLI Commands
FastMCP automatically detects and uses `fastmcp.json` files, making server execution simple and consistent:
```bash
# Auto-detect fastmcp.json in current directory
cd my-project
fastmcp run # No arguments needed!
# Or specify a configuration file explicitly
fastmcp run prod.fastmcp.json
```
The configuration file works with all FastMCP commands:
- **`run`** - Start the server in production mode
- **`dev`** - Launch with the Inspector UI for development
- **`inspect`** - View server capabilities and configuration
- **`install`** - Install to Claude Desktop, Cursor, or other MCP clients
When no file argument is provided, FastMCP searches the current directory for `fastmcp.json`. This means you can simply navigate to your project directory and run `fastmcp run` to start your server with all its configured settings.
### Custom Naming Patterns
You can use different configuration files for different environments:
- `fastmcp.json` - Default configuration
- `dev.fastmcp.json` - Development settings
- `prod.fastmcp.json` - Production settings
- `test_fastmcp.json` - Test configuration
Any file with "fastmcp.json" in the name is recognized as a configuration file.
## Examples
<Tabs>
<Tab title="Basic Configuration">
A minimal configuration for a simple server:
```json
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
}
}
```
This configuration explicitly specifies the server object name (`app`), making it clear which object contains your FastMCP server. Uses all defaults: STDIO transport, no special dependencies, standard logging.
</Tab>
<Tab title="Development Configuration">
A configuration optimized for local development:
```json
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": "src/server.py:app",
"environment": {
"python": "3.12",
"dependencies": ["fastmcp[dev]"],
"editable": "."
},
"deployment": {
"transport": "http",
"host": "127.0.0.1",
"port": 8000,
"log_level": "DEBUG",
"env": {
"DEBUG": "true",
"ENV": "development"
}
}
}
```
</Tab>
<Tab title="Production Configuration">
A production-ready configuration with full dependency management:
```json
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": {
"file": "app/main.py",
"object": "mcp_server"
},
"environment": {
"python": "3.11",
"requirements": "requirements/production.txt",
"project": "."
},
"deployment": {
"transport": "http",
"host": "0.0.0.0",
"port": 3000,
"path": "/api/mcp/",
"log_level": "INFO",
"env": {
"ENV": "production",
"API_BASE_URL": "https://api.example.com",
"DATABASE_URL": "postgresql://user:pass@db.example.com/prod"
},
"cwd": "/app",
"args": ["--workers", "4"]
}
}
```
</Tab>
<Tab title="Data Science Server">
Configuration for a data analysis server with scientific packages:
```json
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": {
"file": "analysis_server.py",
"object": "mcp"
},
"environment": {
"python": "3.11",
"dependencies": [
"pandas>=2.0",
"numpy",
"scikit-learn",
"matplotlib",
"jupyterlab"
]
},
"deployment": {
"transport": "stdio",
"env": {
"MATPLOTLIB_BACKEND": "Agg",
"DATA_PATH": "./datasets"
}
}
}
```
</Tab>
<Tab title="Multi-Environment Setup">
You can maintain multiple configuration files for different environments:
**dev.fastmcp.json**:
```json
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
},
"deployment": {
"transport": "http",
"log_level": "DEBUG"
}
}
```
**prod.fastmcp.json**:
```json
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
},
"environment": {
"requirements": "requirements/production.txt"
},
"deployment": {
"transport": "http",
"host": "0.0.0.0",
"log_level": "WARNING"
}
}
```
Run different configurations:
```bash
fastmcp run dev.fastmcp.json # Development
fastmcp run prod.fastmcp.json # Production
```
</Tab>
</Tabs>
## CLI Override Behavior
Command-line arguments take precedence over configuration file values, allowing ad-hoc adjustments without modifying the file:
```bash
# Config specifies port 3000, CLI overrides to 8080
fastmcp run fastmcp.json --port 8080
# Config specifies stdio, CLI overrides to HTTP
fastmcp run fastmcp.json --transport http
# Add extra dependencies not in config
fastmcp run fastmcp.json --with requests --with httpx
```
This precedence order enables:
- Quick testing of different settings
- Environment-specific overrides in deployment scripts
- Debugging with increased log levels
- Temporary configuration changes
## Best Practices
When using `fastmcp.json` for your projects, consider these recommendations:
**Version Control**: Always commit your `fastmcp.json` to version control. It's essential project documentation that ensures others can run your server correctly.
**Environment Variables**: Use the `env` field for configuration values instead of hardcoding them in your Python code. For sensitive values, consider using environment variable references or separate secret management.
**Dependency Management**: Specify exact versions for production dependencies to ensure reproducible builds:
```json
{
"dependencies": [
"pandas==2.1.0",
"requests==2.31.0"
]
}
```
**Path Resolution**: Remember that paths in the configuration are relative to the config file location. Use relative paths for portability:
```json
{
"entrypoint": "./src/server.py",
"environment": {
"requirements": "./requirements.txt"
}
}
```
**Development Workflow**: Use separate configuration files for different environments rather than constantly modifying a single file. The CLI's override behavior makes it easy to switch between configurations.
### Environment Variable Interpolation
#### Environment Variable Interpolation
The `env` field in deployment configuration supports runtime interpolation of environment variables using `${VAR_NAME}` syntax. This enables dynamic configuration based on your deployment environment:
@ -499,6 +290,266 @@ This feature is particularly useful for:
- Building dynamic URLs and connection strings
- Creating environment-specific prefixes or suffixes
## Usage with CLI Commands
FastMCP automatically detects and uses a file specifically named `fastmcp.json` in the current directory, making server execution simple and consistent. Files with FastMCP configuration format but different names are not auto-detected and must be specified explicitly:
```bash
# Auto-detect fastmcp.json in current directory
cd my-project
fastmcp run # No arguments needed!
# Or specify a configuration file explicitly
fastmcp run prod.fastmcp.json
# Skip environment setup when already in a uv environment
fastmcp run fastmcp.json --skip-env
# Skip source preparation when source is already prepared
fastmcp run fastmcp.json --skip-source
# Skip both environment and source preparation
fastmcp run fastmcp.json --skip-env --skip-source
```
### Using an Existing Environment
By default, FastMCP creates an isolated environment with `uv` based on your configuration. When you already have a suitable Python environment, use the `--skip-env` flag to skip environment creation:
```bash
fastmcp run fastmcp.json --skip-env
```
**When you already have an environment:**
- You're in an activated virtual environment with all dependencies installed
- You're inside a Docker container with pre-installed dependencies
- You're in a CI/CD pipeline that pre-builds the environment
- You're using a system-wide installation with all required packages
- You're in a uv-managed environment (prevents infinite recursion)
This flag tells FastMCP: "I already have everything installed, just run the server."
### Using an Existing Source
When working with source types that require preparation (future support for git repositories or cloud sources), use the `--skip-source` flag when you already have the source code available:
```bash
fastmcp run fastmcp.json --skip-source
```
**When you already have the source:**
- You've previously cloned a git repository and don't need to re-fetch
- You have a cached copy of a cloud-hosted server
- You're in a CI/CD pipeline where source checkout is a separate step
- You're iterating locally on already-downloaded code
This flag tells FastMCP: "I already have the source code, skip any download/clone steps."
Note: For filesystem sources (local Python files), this flag has no effect since they don't require preparation.
The configuration file works with all FastMCP commands:
- **`run`** - Start the server in production mode
- **`dev`** - Launch with the Inspector UI for development
- **`inspect`** - View server capabilities and configuration
- **`install`** - Install to Claude Desktop, Cursor, or other MCP clients
When no file argument is provided, FastMCP searches the current directory for `fastmcp.json`. This means you can simply navigate to your project directory and run `fastmcp run` to start your server with all its configured settings.
### CLI Override Behavior
Command-line arguments take precedence over configuration file values, allowing ad-hoc adjustments without modifying the file:
```bash
# Config specifies port 3000, CLI overrides to 8080
fastmcp run fastmcp.json --port 8080
# Config specifies stdio, CLI overrides to HTTP
fastmcp run fastmcp.json --transport http
# Add extra dependencies not in config
fastmcp run fastmcp.json --with requests --with httpx
```
This precedence order enables:
- Quick testing of different settings
- Environment-specific overrides in deployment scripts
- Debugging with increased log levels
- Temporary configuration changes
### Custom Naming Patterns
You can use different configuration files for different environments:
- `fastmcp.json` - Default configuration
- `dev.fastmcp.json` - Development settings
- `prod.fastmcp.json` - Production settings
- `test_fastmcp.json` - Test configuration
Any file with "fastmcp.json" in the name is recognized as a configuration file.
## Examples
<Tabs>
<Tab title="Basic Configuration">
A minimal configuration for a simple server:
```json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "server.py",
"entrypoint": "mcp"
}
}
```
This configuration explicitly specifies the server entrypoint (`mcp`), making it clear which server instance or factory function to use. Uses all defaults: STDIO transport, no special dependencies, standard logging.
</Tab>
<Tab title="Development Configuration">
A configuration optimized for local development:
```json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
// WHERE does the server live?
"source": {
"path": "src/server.py",
"entrypoint": "app"
},
// WHAT dependencies does it need?
"environment": {
"python": "3.12",
"dependencies": ["fastmcp[dev]"],
"editable": "."
},
// HOW should it run?
"deployment": {
"transport": "http",
"host": "127.0.0.1",
"port": 8000,
"log_level": "DEBUG",
"env": {
"DEBUG": "true",
"ENV": "development"
}
}
}
```
</Tab>
<Tab title="Production Configuration">
A production-ready configuration with full dependency management:
```json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
// WHERE does the server live?
"source": {
"path": "app/main.py",
"entrypoint": "mcp_server"
},
// WHAT dependencies does it need?
"environment": {
"python": "3.11",
"requirements": "requirements/production.txt",
"project": "."
},
// HOW should it run?
"deployment": {
"transport": "http",
"host": "0.0.0.0",
"port": 3000,
"path": "/api/mcp/",
"log_level": "INFO",
"env": {
"ENV": "production",
"API_BASE_URL": "https://api.example.com",
"DATABASE_URL": "postgresql://user:pass@db.example.com/prod"
},
"cwd": "/app",
"args": ["--workers", "4"]
}
}
```
</Tab>
<Tab title="Data Science Server">
Configuration for a data analysis server with scientific packages:
```json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "analysis_server.py",
"entrypoint": "mcp"
},
"environment": {
"python": "3.11",
"dependencies": [
"pandas>=2.0",
"numpy",
"scikit-learn",
"matplotlib",
"jupyterlab"
]
},
"deployment": {
"transport": "stdio",
"env": {
"MATPLOTLIB_BACKEND": "Agg",
"DATA_PATH": "./datasets"
}
}
}
```
</Tab>
<Tab title="Multi-Environment Setup">
You can maintain multiple configuration files for different environments:
**dev.fastmcp.json**:
```json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "server.py",
"entrypoint": "mcp"
},
"deployment": {
"transport": "http",
"log_level": "DEBUG"
}
}
```
**prod.fastmcp.json**:
```json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
"requirements": "requirements/production.txt"
},
"deployment": {
"transport": "http",
"host": "0.0.0.0",
"log_level": "WARNING"
}
}
```
Run different configurations:
```bash
fastmcp run dev.fastmcp.json # Development
fastmcp run prod.fastmcp.json # Production
```
</Tab>
</Tabs>
## Migrating from CLI Arguments
If you're currently using command-line arguments or shell scripts, migrating to `fastmcp.json` simplifies your workflow. Here's how common CLI patterns map to configuration:
@ -515,10 +566,10 @@ uv run --with pandas --with requests \
**Equivalent fastmcp.json**:
```json
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
"dependencies": ["pandas", "requests"]

View file

@ -119,7 +119,10 @@
{
"group": "Essentials",
"icon": "cube",
"pages": ["clients/client", "clients/transports"]
"pages": [
"clients/client",
"clients/transports"
]
},
{
"group": "Core Operations",
@ -145,7 +148,10 @@
{
"group": "Authentication",
"icon": "user-shield",
"pages": ["clients/auth/oauth", "clients/auth/bearer"]
"pages": [
"clients/auth/oauth",
"clients/auth/bearer"
]
}
]
},
@ -224,12 +230,17 @@
},
{
"anchor": "What's New",
"pages": ["updates", "changelog"]
"pages": [
"updates",
"changelog"
]
},
{
"anchor": "Community",
"icon": "users",
"pages": ["community/showcase"]
"pages": [
"community/showcase"
]
}
],
"tab": "Documentation"
@ -327,6 +338,7 @@
"python-sdk/fastmcp-server-auth-providers-workos"
]
},
"python-sdk/fastmcp-server-auth-redirect_validation",
"python-sdk/fastmcp-server-auth-registry"
]
},
@ -367,6 +379,7 @@
"python-sdk/fastmcp-utilities-cli",
"python-sdk/fastmcp-utilities-components",
"python-sdk/fastmcp-utilities-exceptions",
"python-sdk/fastmcp-utilities-fastmcp_config",
"python-sdk/fastmcp-utilities-http",
"python-sdk/fastmcp-utilities-inspect",
"python-sdk/fastmcp-utilities-json_schema",

View file

@ -85,10 +85,10 @@ Alternatively, you can use a `fastmcp.json` configuration file (recommended):
```json fastmcp.json
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
"dependencies": ["pandas", "requests"]

View file

@ -102,10 +102,10 @@ Alternatively, you can use a `fastmcp.json` configuration file (recommended):
```json fastmcp.json
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
"dependencies": ["pandas", "requests"]

View file

@ -103,10 +103,10 @@ Alternatively, you can use a `fastmcp.json` configuration file (recommended):
```json fastmcp.json
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
"dependencies": ["pandas", "requests"]

View file

@ -178,10 +178,10 @@ You can also use a `fastmcp.json` configuration file (recommended):
```json fastmcp.json
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
"dependencies": ["pandas", "matplotlib", "seaborn"]

View file

@ -46,6 +46,7 @@ By default, this command runs the server directly in your current Python environ
| Path | `--path` | Path to bind to when using http transport (default: `/mcp/` or `/sse/` for SSE) |
| Log Level | `--log-level`, `-l` | Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
| No Banner | `--no-banner` | Disable the startup banner display |
| No Environment | `--skip-env` | Skip environment setup with uv (use when already in a uv environment) |
| Python Version | `--python` | Python version to use (e.g., 3.10, 3.11) |
| Additional Packages | `--with` | Additional packages to install (can be used multiple times) |
| Project Directory | `--project` | Run the command within the given project directory |
@ -57,8 +58,8 @@ By default, this command runs the server directly in your current Python environ
The `fastmcp run` command supports the following entrypoints:
1. **[Inferred server instance](#inferred-server-instance)**: `server.py` - imports the module and looks for a FastMCP object named `mcp`, `server`, or `app`. Errors if no such object is found.
2. **[Explicit server object](#explicit-server-object)**: `server.py:custom_name` - imports and uses the specified server object
1. **[Inferred server instance](#inferred-server-instance)**: `server.py` - imports the module and looks for a FastMCP server instance named `mcp`, `server`, or `app`. Errors if no such object is found.
2. **[Explicit server entrypoint](#explicit-server-entrypoint)**: `server.py:custom_name` - imports and uses the specified server entrypoint
3. **[Factory function](#factory-function)**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance
4. **[Remote server proxy](#remote-server-proxy)**: `https://example.com/mcp-server` - connects to a remote server and creates a **local proxy server**
5. **[FastMCP configuration file](#fastmcp-configuration)**: `fastmcp.json` - runs servers using FastMCP's declarative configuration format (auto-detects files in current directory)
@ -68,7 +69,7 @@ The `fastmcp run` command supports the following entrypoints:
Note: When using `fastmcp run` with a local file, it **completely ignores** the `if __name__ == "__main__"` block. This means:
- Any setup code in `__main__` will NOT run
- Server configuration in `__main__` is bypassed
- `fastmcp run` finds your server object/factory and runs it with its own transport settings
- `fastmcp run` finds your server entrypoint/factory and runs it with its own transport settings
If you need setup code to run, use the **factory pattern** instead.
</Warning>
@ -91,9 +92,9 @@ You can run it with:
fastmcp run server.py
```
#### Explicit Server Object
#### Explicit Server Entrypoint
If your server is stored as a variable with a custom name, or you want to be explicit about which server to run, you can use the following syntax to load a specific server object:
If your server is stored as a variable with a custom name, or you want to be explicit about which server to run, you can use the following syntax to load a specific server entrypoint:
```bash
fastmcp run server.py:custom_name
@ -122,7 +123,7 @@ fastmcp run server.py:custom_name
Since `fastmcp run` ignores the `if __name__ == "__main__"` block, you can use a factory function to run setup code before your server starts. Factory functions are called without any arguments and must return a FastMCP server instance. Both sync and async factory functions are supported.
The syntax for using a factory function is the same as for an explicit server object: `fastmcp run server.py:factory_fn`. FastMCP will automatically detect that you have identified a function rather than a server Instance
The syntax for using a factory function is the same as for an explicit server entrypoint: `fastmcp run server.py:factory_fn`. FastMCP will automatically detect that you have identified a function rather than a server Instance
For example, if you have a file called `server.py` with the following content:
@ -177,8 +178,19 @@ The configuration file handles dependencies, environment variables, and transpor
```bash
# Override port from config file
fastmcp run fastmcp.json --port 8080
# Skip environment setup when already in a uv environment
fastmcp run fastmcp.json --skip-env
```
<Note>
The `--skip-env` flag is useful when:
- You're already in an activated virtual environment
- You're inside a Docker container with pre-installed dependencies
- You're in a uv-managed environment (prevents infinite recursion)
- You want to test the server without environment setup
</Note>
See [Server Configuration](/deployment/server-configuration) for detailed documentation on fastmcp.json.
#### MCP Configuration
@ -244,8 +256,8 @@ This command does not support HTTP testing. To test a server over Streamable HTT
The `dev` command supports local FastMCP server files and configuration:
1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP object named `mcp`, `server`, or `app`. Errors if no such object is found.
2. **Explicit server object**: `server.py:custom_name` - imports and uses the specified server object
1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP server instance named `mcp`, `server`, or `app`. Errors if no such object is found.
2. **Explicit server entrypoint**: `server.py:custom_name` - imports and uses the specified server entrypoint
3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance
4. **FastMCP configuration**: `fastmcp.json` - uses FastMCP's declarative configuration (auto-detects in current directory)
@ -323,8 +335,8 @@ Note that for security reasons, MCP clients usually run every server in a comple
The `install` command supports local FastMCP server files and configuration:
1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP object named `mcp`, `server`, or `app`. Errors if no such object is found.
2. **Explicit server object**: `server.py:custom_name` - imports and uses the specified server object
1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP server instance named `mcp`, `server`, or `app`. Errors if no such object is found.
2. **Explicit server entrypoint**: `server.py:custom_name` - imports and uses the specified server entrypoint
3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance
4. **FastMCP configuration**: `fastmcp.json` - uses FastMCP's declarative configuration with dependencies and settings
@ -339,7 +351,7 @@ The `install` command **only supports local files and fastmcp.json** - no URLs,
**Examples**
```bash
# Auto-detects server object (looks for 'mcp', 'server', or 'app')
# Auto-detects server entrypoint (looks for 'mcp', 'server', or 'app')
fastmcp install claude-desktop server.py
# Install with fastmcp.json configuration (auto-detects)
@ -348,7 +360,7 @@ fastmcp install claude-desktop
# Install with explicit fastmcp.json file
fastmcp install claude-desktop my-config.fastmcp.json
# Uses specific server object
# Uses specific server entrypoint
fastmcp install claude-desktop server.py:my_server
# With custom name and dependencies
@ -439,8 +451,8 @@ fastmcp inspect server.py
The `inspect` command supports local FastMCP server files and configuration:
1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP object named `mcp`, `server`, or `app`. Errors if no such object is found.
2. **Explicit server object**: `server.py:custom_name` - imports and uses the specified server object
1. **Inferred server instance**: `server.py` - imports the module and looks for a FastMCP server instance named `mcp`, `server`, or `app`. Errors if no such object is found.
2. **Explicit server entrypoint**: `server.py:custom_name` - imports and uses the specified server entrypoint
3. **Factory function**: `server.py:create_server` - calls the specified function (sync or async) to create a server instance
4. **FastMCP configuration**: `fastmcp.json` - inspects servers defined with FastMCP's declarative configuration
@ -451,10 +463,10 @@ The `inspect` command **only supports local files and fastmcp.json** - no URLs,
**Examples**
```bash
# Auto-detect server object
# Auto-detect server entrypoint
fastmcp inspect server.py
# Specify server object
# Specify server entrypoint
fastmcp inspect server.py:my_server
# Custom output location

View file

@ -0,0 +1,348 @@
{
"$defs": {
"Deployment": {
"description": "Configuration for server deployment and runtime settings.",
"properties": {
"transport": {
"anyOf": [
{
"enum": [
"stdio",
"http",
"sse"
],
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Transport protocol to use",
"title": "Transport"
},
"host": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Host to bind to when using HTTP transport",
"examples": [
"127.0.0.1",
"0.0.0.0",
"localhost"
],
"title": "Host"
},
"port": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Port to bind to when using HTTP transport",
"examples": [
8000,
3000,
5000
],
"title": "Port"
},
"path": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "URL path for the server endpoint",
"examples": [
"/mcp/",
"/api/mcp/",
"/sse/"
],
"title": "Path"
},
"log_level": {
"anyOf": [
{
"enum": [
"DEBUG",
"INFO",
"WARNING",
"ERROR",
"CRITICAL"
],
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Log level for the server",
"title": "Log Level"
},
"cwd": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Working directory for the server process",
"examples": [
".",
"./src",
"/app"
],
"title": "Cwd"
},
"env": {
"anyOf": [
{
"additionalProperties": {
"type": "string"
},
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "Environment variables to set when running the server",
"examples": [
{
"API_KEY": "secret",
"DEBUG": "true"
}
],
"title": "Env"
},
"args": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Arguments to pass to the server (after --)",
"examples": [
[
"--config",
"config.json",
"--debug"
]
],
"title": "Args"
}
},
"title": "Deployment",
"type": "object"
},
"Environment": {
"description": "Configuration for Python environment setup.",
"properties": {
"python": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Python version constraint",
"examples": [
"3.10",
"3.11",
"3.12"
],
"title": "Python"
},
"dependencies": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Python packages to install with PEP 508 specifiers",
"examples": [
[
"fastmcp>=2.0,<3",
"httpx",
"pandas>=2.0"
]
],
"title": "Dependencies"
},
"requirements": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Path to requirements.txt file",
"examples": [
"requirements.txt",
"../requirements/prod.txt"
],
"title": "Requirements"
},
"project": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Path to project directory containing pyproject.toml",
"examples": [
".",
"../my-project"
],
"title": "Project"
},
"editable": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Directory to install in editable mode",
"examples": [
".",
"../my-package"
],
"title": "Editable"
}
},
"title": "Environment",
"type": "object"
},
"FileSystemSource": {
"description": "Source for local Python files.",
"properties": {
"type": {
"const": "filesystem",
"default": "filesystem",
"description": "Source type",
"title": "Type",
"type": "string"
},
"path": {
"description": "Path to Python file containing the server",
"title": "Path",
"type": "string"
},
"entrypoint": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
"title": "Entrypoint"
}
},
"required": [
"path"
],
"title": "FileSystemSource",
"type": "object"
}
},
"description": "Configuration file for FastMCP servers",
"properties": {
"$schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"description": "JSON schema for IDE support and validation",
"title": "$Schema"
},
"source": {
"$ref": "#/$defs/FileSystemSource",
"description": "Source configuration for the server",
"examples": [
{
"path": "server.py"
},
{
"entrypoint": "app",
"path": "server.py"
},
{
"entrypoint": "mcp",
"path": "src/server.py",
"type": "filesystem"
}
]
},
"environment": {
"$ref": "#/$defs/Environment",
"description": "Python environment setup configuration"
},
"deployment": {
"$ref": "#/$defs/Deployment",
"description": "Server deployment and runtime settings"
}
},
"required": [
"source"
],
"title": "FastMCP Configuration",
"type": "object",
"$id": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json"
}

View file

@ -0,0 +1,348 @@
{
"$defs": {
"Deployment": {
"description": "Configuration for server deployment and runtime settings.",
"properties": {
"transport": {
"anyOf": [
{
"enum": [
"stdio",
"http",
"sse"
],
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Transport protocol to use",
"title": "Transport"
},
"host": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Host to bind to when using HTTP transport",
"examples": [
"127.0.0.1",
"0.0.0.0",
"localhost"
],
"title": "Host"
},
"port": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Port to bind to when using HTTP transport",
"examples": [
8000,
3000,
5000
],
"title": "Port"
},
"path": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "URL path for the server endpoint",
"examples": [
"/mcp/",
"/api/mcp/",
"/sse/"
],
"title": "Path"
},
"log_level": {
"anyOf": [
{
"enum": [
"DEBUG",
"INFO",
"WARNING",
"ERROR",
"CRITICAL"
],
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Log level for the server",
"title": "Log Level"
},
"cwd": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Working directory for the server process",
"examples": [
".",
"./src",
"/app"
],
"title": "Cwd"
},
"env": {
"anyOf": [
{
"additionalProperties": {
"type": "string"
},
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "Environment variables to set when running the server",
"examples": [
{
"API_KEY": "secret",
"DEBUG": "true"
}
],
"title": "Env"
},
"args": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Arguments to pass to the server (after --)",
"examples": [
[
"--config",
"config.json",
"--debug"
]
],
"title": "Args"
}
},
"title": "Deployment",
"type": "object"
},
"Environment": {
"description": "Configuration for Python environment setup.",
"properties": {
"python": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Python version constraint",
"examples": [
"3.10",
"3.11",
"3.12"
],
"title": "Python"
},
"dependencies": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"description": "Python packages to install with PEP 508 specifiers",
"examples": [
[
"fastmcp>=2.0,<3",
"httpx",
"pandas>=2.0"
]
],
"title": "Dependencies"
},
"requirements": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Path to requirements.txt file",
"examples": [
"requirements.txt",
"../requirements/prod.txt"
],
"title": "Requirements"
},
"project": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Path to project directory containing pyproject.toml",
"examples": [
".",
"../my-project"
],
"title": "Project"
},
"editable": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Directory to install in editable mode",
"examples": [
".",
"../my-package"
],
"title": "Editable"
}
},
"title": "Environment",
"type": "object"
},
"FileSystemSource": {
"description": "Source for local Python files.",
"properties": {
"type": {
"const": "filesystem",
"default": "filesystem",
"description": "Source type",
"title": "Type",
"type": "string"
},
"path": {
"description": "Path to Python file containing the server",
"title": "Path",
"type": "string"
},
"entrypoint": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
"title": "Entrypoint"
}
},
"required": [
"path"
],
"title": "FileSystemSource",
"type": "object"
}
},
"description": "Configuration file for FastMCP servers",
"properties": {
"$schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"description": "JSON schema for IDE support and validation",
"title": "$Schema"
},
"source": {
"$ref": "#/$defs/FileSystemSource",
"description": "Source configuration for the server",
"examples": [
{
"path": "server.py"
},
{
"entrypoint": "app",
"path": "server.py"
},
{
"entrypoint": "mcp",
"path": "src/server.py",
"type": "filesystem"
}
]
},
"environment": {
"$ref": "#/$defs/Environment",
"description": "Python environment setup configuration"
},
"deployment": {
"$ref": "#/$defs/Deployment",
"description": "Server deployment and runtime settings"
}
},
"required": [
"source"
],
"title": "FastMCP Configuration",
"type": "object",
"$id": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json"
}

View file

@ -10,7 +10,7 @@ Claude app integration utilities.
## Functions
### `get_claude_config_path` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/claude.py#L14" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `get_claude_config_path` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/claude.py#L15" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_claude_config_path() -> Path | None
@ -20,7 +20,7 @@ get_claude_config_path() -> Path | None
Get the Claude config directory based on platform.
### `update_claude_config` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/claude.py#L32" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `update_claude_config` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/claude.py#L33" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
update_claude_config(file_spec: str, server_name: str) -> bool

View file

@ -10,7 +10,7 @@ FastMCP CLI tools using Cyclopts.
## Functions
### `version` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L103" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `version` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
version()
@ -20,45 +20,47 @@ version()
Display version information and platform details.
### `dev` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L141" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `dev` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L101" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
dev(server_spec: str) -> None
dev(server_spec: str | None = None) -> None
```
Run an MCP server with the MCP Inspector for development.
**Args:**
- `server_spec`: Python file to run, optionally with \:object suffix
- `server_spec`: Python file to run, optionally with \:object suffix, or None to auto-detect fastmcp.json
### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L286" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L319" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run(server_spec: str, *server_args: str) -> None
run(server_spec: str | None = None, *server_args: str) -> None
```
Run an MCP server or connect to a remote one.
The server can be specified in four ways:
The server can be specified in several ways:
1. Module approach: "server.py" - runs the module directly, looking for an object named 'mcp', 'server', or 'app'
2. Import approach: "server.py:app" - imports and runs the specified server object
3. URL approach: "http://server-url" - connects to a remote server and creates a proxy
4. MCPConfig file: "mcp.json" - runs as a proxy server for the MCP Servers in the MCPConfig file
5. FastMCP config: "fastmcp.json" - runs server using FastMCP configuration
6. No argument: looks for fastmcp.json in current directory
Server arguments can be passed after -- :
fastmcp run server.py -- --config config.json --debug
**Args:**
- `server_spec`: Python file, object specification (file\:obj), MCPConfig file, or URL
- `server_spec`: Python file, object specification (file\:obj), config file, URL, or None to auto-detect
### `inspect` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L439" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `inspect` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L582" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
inspect(server_spec: str) -> None
inspect(server_spec: str | None = None) -> None
```
@ -74,7 +76,9 @@ fastmcp inspect server.py
fastmcp inspect server.py -o report.json
fastmcp inspect server.py:mcp -o analysis.json
fastmcp inspect path/to/server.py:app -o /tmp/server-info.json
fastmcp inspect fastmcp.json
fastmcp inspect # auto-detect fastmcp.json
**Args:**
- `server_spec`: Python file to inspect, optionally with \:object suffix
- `server_spec`: Python file to inspect, optionally with \:object suffix, or fastmcp.json

View file

@ -10,7 +10,7 @@ Claude Code integration for FastMCP install using Cyclopts.
## Functions
### `find_claude_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_code.py#L19" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `find_claude_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_code.py#L20" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
find_claude_command() -> str | None
@ -23,7 +23,7 @@ Checks common installation locations since 'claude' is often a shell alias
that doesn't work with subprocess calls.
### `check_claude_code_available` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_code.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `check_claude_code_available` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_code.py#L68" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
check_claude_code_available() -> bool
@ -33,7 +33,7 @@ check_claude_code_available() -> bool
Check if Claude Code CLI is available.
### `install_claude_code` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_code.py#L72" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `install_claude_code` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_code.py#L73" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
install_claude_code(file: Path, server_object: str | None, name: str) -> bool
@ -57,7 +57,7 @@ Install FastMCP server in Claude Code.
- True if installation was successful, False otherwise
### `claude_code_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_code.py#L170" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `claude_code_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_code.py#L163" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
claude_code_command(server_spec: str) -> None

View file

@ -10,7 +10,7 @@ Claude Desktop integration for FastMCP install using Cyclopts.
## Functions
### `get_claude_config_path` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_desktop.py#L19" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `get_claude_config_path` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_desktop.py#L20" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_claude_config_path() -> Path | None
@ -20,7 +20,7 @@ get_claude_config_path() -> Path | None
Get the Claude config directory based on platform.
### `install_claude_desktop` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_desktop.py#L37" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `install_claude_desktop` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_desktop.py#L38" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
install_claude_desktop(file: Path, server_object: str | None, name: str) -> bool
@ -44,7 +44,7 @@ Install FastMCP server in Claude Desktop.
- True if installation was successful, False otherwise
### `claude_desktop_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_desktop.py#L143" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `claude_desktop_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_desktop.py#L135" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
claude_desktop_command(server_spec: str) -> None

View file

@ -10,7 +10,7 @@ Cursor integration for FastMCP install using Cyclopts.
## Functions
### `generate_cursor_deeplink` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L20" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `generate_cursor_deeplink` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L21" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
generate_cursor_deeplink(server_name: str, server_config: StdioMCPServer) -> str
@ -27,7 +27,7 @@ Generate a Cursor deeplink for installing the MCP server.
- Deeplink URL that can be clicked to install the server
### `open_deeplink` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L44" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `open_deeplink` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L45" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
open_deeplink(deeplink: str) -> bool
@ -43,7 +43,32 @@ Attempt to open a deeplink URL using the system's default handler.
- True if the command succeeded, False otherwise
### `install_cursor` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `install_cursor_workspace` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L68" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
install_cursor_workspace(file: Path, server_object: str | None, name: str, workspace_path: Path) -> bool
```
Install FastMCP server to workspace-specific Cursor configuration.
**Args:**
- `file`: Path to the server file
- `server_object`: Optional server object name (for \:object suffix)
- `name`: Name for the server in Cursor
- `workspace_path`: Path to the workspace directory
- `with_editable`: Optional directory to install in editable mode
- `with_packages`: Optional list of additional packages to install
- `env_vars`: Optional dictionary of environment variables
- `python_version`: Optional Python version to use
- `with_requirements`: Optional requirements file to install from
- `project`: Optional project directory to run within
**Returns:**
- True if installation was successful, False otherwise
### `install_cursor` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L159" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
install_cursor(file: Path, server_object: str | None, name: str) -> bool
@ -62,12 +87,13 @@ Install FastMCP server in Cursor.
- `python_version`: Optional Python version to use
- `with_requirements`: Optional requirements file to install from
- `project`: Optional project directory to run within
- `workspace`: Optional workspace directory for project-specific installation
**Returns:**
- True if installation was successful, False otherwise
### `cursor_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L153" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `cursor_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L254" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
cursor_command(server_spec: str) -> None

View file

@ -10,7 +10,7 @@ MCP configuration JSON generation for FastMCP install using Cyclopts.
## Functions
### `install_mcp_json` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/mcp_json.py#L19" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `install_mcp_json` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/mcp_json.py#L20" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
install_mcp_json(file: Path, server_object: str | None, name: str) -> bool
@ -35,7 +35,7 @@ Generate MCP configuration JSON for manual installation.
- True if generation was successful, False otherwise
### `mcp_json_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/mcp_json.py#L116" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `mcp_json_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/mcp_json.py#L108" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
mcp_json_command(server_spec: str) -> None

View file

@ -10,7 +10,7 @@ Shared utilities for install commands.
## Functions
### `parse_env_var` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/shared.py#L15" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `parse_env_var` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/shared.py#L18" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
parse_env_var(env_var: str) -> tuple[str, str]
@ -20,7 +20,7 @@ parse_env_var(env_var: str) -> tuple[str, str]
Parse environment variable string in format KEY=VALUE.
### `process_common_args` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/shared.py#L26" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `process_common_args` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/shared.py#L29" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
process_common_args(server_spec: str, server_name: str | None, with_packages: list[str], env_vars: list[str], env_file: Path | None) -> tuple[Path, str | None, str, list[str], dict[str, str] | None]
@ -29,3 +29,5 @@ process_common_args(server_spec: str, server_name: str | None, with_packages: li
Process common arguments shared by all install commands.
Handles both fastmcp.json config files and traditional file.py:object syntax.

View file

@ -10,7 +10,7 @@ FastMCP run command implementation with enhanced type hints.
## Functions
### `is_url` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L25" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `is_url` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L32" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
is_url(path: str) -> bool
@ -20,7 +20,7 @@ is_url(path: str) -> bool
Check if a string is a URL.
### `parse_file_path` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L31" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `parse_file_path` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L38" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
parse_file_path(server_spec: str) -> tuple[Path, str | None]
@ -36,7 +36,7 @@ Parse a file path that may include a server object specification.
- Tuple of (file_path, server_object)
### `import_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L62" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `import_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L69" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
import_server(file: Path, server_or_factory: str | None = None) -> Any
@ -53,17 +53,17 @@ Import a MCP server from a file.
- The server object (or result of calling a factory function)
### `run_with_uv` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L174" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `run_with_uv` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L182" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run_with_uv(server_spec: str, python_version: str | None = None, with_packages: list[str] | None = None, with_requirements: Path | None = None, project: Path | None = None, transport: TransportType | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: LogLevelType | None = None, show_banner: bool = True) -> None
run_with_uv(server_spec: str, python_version: str | None = None, with_packages: list[str] | None = None, with_requirements: Path | None = None, project: Path | None = None, transport: TransportType | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: LogLevelType | None = None, show_banner: bool = True, editable: str | None = None) -> None
```
Run a MCP server using uv run subprocess.
**Args:**
- `server_spec`: Python file, object specification (file\:obj), or URL
- `server_spec`: Python file, object specification (file\:obj), config file, or URL
- `python_version`: Python version to use (e.g. "3.10")
- `with_packages`: Additional packages to install
- `with_requirements`: Requirements file to use
@ -76,7 +76,7 @@ Run a MCP server using uv run subprocess.
- `show_banner`: Whether to show the server banner
### `create_client_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L252" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `create_client_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L313" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_client_server(url: str) -> Any
@ -92,7 +92,7 @@ Create a FastMCP server from a client URL.
- A FastMCP server instance
### `create_mcp_config_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L272" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `create_mcp_config_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L333" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]
@ -102,7 +102,23 @@ create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]
Create a FastMCP server from a MCPConfig.
### `import_server_with_args` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L283" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `load_fastmcp_config` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L344" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
load_fastmcp_config(config_path: Path) -> FastMCPConfig
```
Load a FastMCP configuration from a fastmcp.json file.
**Args:**
- `config_path`: Path to fastmcp.json file
**Returns:**
- FastMCPConfig object
### `import_server_with_args` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L362" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
import_server_with_args(file: Path, server_or_factory: str | None = None, server_args: list[str] | None = None) -> Any
@ -120,7 +136,7 @@ Import a server with optional command line arguments.
- The imported server object
### `run_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L309" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `run_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L388" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run_command(server_spec: str, transport: TransportType | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: LogLevelType | None = None, server_args: list[str] | None = None, show_banner: bool = True, use_direct_import: bool = False) -> None
@ -130,7 +146,7 @@ run_command(server_spec: str, transport: TransportType | None = None, host: str
Run a MCP server or connect to a remote one.
**Args:**
- `server_spec`: Python file, object specification (file\:obj), MCPConfig file, or URL
- `server_spec`: Python file, object specification (file\:obj), config file, or URL
- `transport`: Transport protocol to use
- `host`: Host to bind to when using http transport
- `port`: Port to bind to when using http transport
@ -141,7 +157,7 @@ Run a MCP server or connect to a remote one.
- `use_direct_import`: Whether to use direct import instead of subprocess
### `run_v1_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L374" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `run_v1_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L496" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run_v1_server(server: FastMCP1x, host: str | None = None, port: int | None = None, transport: TransportType | None = None) -> None

View file

@ -122,7 +122,7 @@ 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#L250" 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#L255" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
redirect_handler(self, authorization_url: str) -> None
@ -131,7 +131,7 @@ redirect_handler(self, authorization_url: str) -> None
Open browser for authorization.
#### `callback_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L255" 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#L260" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
callback_handler(self) -> tuple[str, str | None]

View file

@ -7,7 +7,7 @@ sidebarTitle: client
## Classes
### `ClientSessionState` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L74" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ClientSessionState` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L80" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Holds all session-related state for a Client instance.
@ -16,7 +16,7 @@ This allows clean separation of configuration (which is copied) from
session state (which should be fresh for each new client instance).
### `Client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L90" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `Client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L96" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
MCP client that delegates connection management to a Transport instance.
@ -79,7 +79,7 @@ async with client:
**Methods:**
#### `session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L273" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L282" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
session(self) -> ClientSession
@ -88,7 +88,7 @@ session(self) -> ClientSession
Get the current active session. Raises RuntimeError if not connected.
#### `initialize_result` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L283" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `initialize_result` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L292" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
initialize_result(self) -> mcp.types.InitializeResult
@ -97,7 +97,7 @@ initialize_result(self) -> mcp.types.InitializeResult
Get the result of the initialization request.
#### `set_roots` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L291" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `set_roots` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L300" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_roots(self, roots: RootsList | RootsHandler) -> None
@ -106,16 +106,16 @@ set_roots(self, roots: RootsList | RootsHandler) -> None
Set the roots for the client. This does not automatically call `send_roots_list_changed`.
#### `set_sampling_callback` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L295" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `set_sampling_callback` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L304" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_sampling_callback(self, sampling_callback: SamplingHandler) -> None
set_sampling_callback(self, sampling_callback: ClientSamplingHandler) -> None
```
Set the sampling callback for the client.
#### `set_elicitation_callback` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L301" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `set_elicitation_callback` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L310" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_elicitation_callback(self, elicitation_callback: ElicitationHandler) -> None
@ -124,7 +124,7 @@ set_elicitation_callback(self, elicitation_callback: ElicitationHandler) -> None
Set the elicitation callback for the client.
#### `is_connected` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L309" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `is_connected` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L318" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
is_connected(self) -> bool
@ -133,7 +133,7 @@ is_connected(self) -> bool
Check if the client is currently connected.
#### `new` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L313" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `new` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L322" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
new(self) -> Client[ClientTransportT]
@ -149,13 +149,13 @@ share state with the original client.
- A new Client instance with the same configuration but disconnected state.
#### `close` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L476" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `close` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L487" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
close(self)
```
#### `ping` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L482" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `ping` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L493" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
ping(self) -> bool
@ -164,7 +164,7 @@ ping(self) -> bool
Send a ping request.
#### `cancel` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L487" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `cancel` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L498" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
cancel(self, request_id: str | int, reason: str | None = None) -> None
@ -173,7 +173,7 @@ cancel(self, request_id: str | int, reason: str | None = None) -> None
Send a cancellation notification for an in-progress request.
#### `progress` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L504" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `progress` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L515" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
progress(self, progress_token: str | int, progress: float, total: float | None = None, message: str | None = None) -> None
@ -182,7 +182,7 @@ progress(self, progress_token: str | int, progress: float, total: float | None =
Send a progress notification.
#### `set_logging_level` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L516" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `set_logging_level` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L527" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_logging_level(self, level: mcp.types.LoggingLevel) -> None
@ -191,7 +191,7 @@ set_logging_level(self, level: mcp.types.LoggingLevel) -> None
Send a logging/setLevel request.
#### `send_roots_list_changed` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L520" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `send_roots_list_changed` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L531" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
send_roots_list_changed(self) -> None
@ -200,7 +200,7 @@ send_roots_list_changed(self) -> None
Send a roots/list_changed notification.
#### `list_resources_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L526" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_resources_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L537" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resources_mcp(self) -> mcp.types.ListResourcesResult
@ -216,7 +216,7 @@ containing the list of resources and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
#### `list_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L539" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L552" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resources(self) -> list[mcp.types.Resource]
@ -231,7 +231,7 @@ Retrieve a list of resources available on the server.
- `RuntimeError`: If called while the client is not connected.
#### `list_resource_templates_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L551" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_resource_templates_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L564" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resource_templates_mcp(self) -> mcp.types.ListResourceTemplatesResult
@ -247,7 +247,7 @@ containing the list of resource templates and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
#### `list_resource_templates` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L566" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_resource_templates` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L581" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resource_templates(self) -> list[mcp.types.ResourceTemplate]
@ -262,7 +262,7 @@ Retrieve a list of resource templates available on the server.
- `RuntimeError`: If called while the client is not connected.
#### `read_resource_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L580" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read_resource_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L595" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource_mcp(self, uri: AnyUrl | str) -> mcp.types.ReadResourceResult
@ -281,7 +281,7 @@ containing the resource contents and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L600" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L617" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource(self, uri: AnyUrl | str) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]
@ -300,7 +300,7 @@ objects, typically containing either text or binary data.
- `RuntimeError`: If called while the client is not connected.
#### `list_prompts_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L639" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_prompts_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L656" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_prompts_mcp(self) -> mcp.types.ListPromptsResult
@ -316,7 +316,7 @@ containing the list of prompts and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
#### `list_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L652" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L671" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_prompts(self) -> list[mcp.types.Prompt]
@ -331,7 +331,7 @@ Retrieve a list of prompts available on the server.
- `RuntimeError`: If called while the client is not connected.
#### `get_prompt_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L665" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_prompt_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L684" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompt_mcp(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult
@ -351,7 +351,7 @@ containing the prompt messages and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L699" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L720" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult
@ -371,7 +371,7 @@ containing the prompt messages and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
#### `complete_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L720" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `complete_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L741" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
complete_mcp(self, ref: mcp.types.ResourceReference | mcp.types.PromptReference, argument: dict[str, str]) -> mcp.types.CompleteResult
@ -391,7 +391,7 @@ containing the completion and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
#### `complete` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L741" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `complete` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L764" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
complete(self, ref: mcp.types.ResourceReference | mcp.types.PromptReference, argument: dict[str, str]) -> mcp.types.Completion
@ -410,7 +410,7 @@ Send a completion request to the server.
- `RuntimeError`: If called while the client is not connected.
#### `list_tools_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L763" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_tools_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L786" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_tools_mcp(self) -> mcp.types.ListToolsResult
@ -426,7 +426,7 @@ containing the list of tools and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
#### `list_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L776" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L801" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_tools(self) -> list[mcp.types.Tool]
@ -441,7 +441,7 @@ Retrieve a list of tools available on the server.
- `RuntimeError`: If called while the client is not connected.
#### `call_tool_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L790" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `call_tool_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L815" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
call_tool_mcp(self, name: str, arguments: dict[str, Any], progress_handler: ProgressHandler | None = None, timeout: datetime.timedelta | float | int | None = None) -> mcp.types.CallToolResult
@ -466,7 +466,7 @@ containing the tool result and any additional metadata.
- `RuntimeError`: If called while the client is not connected.
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L826" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L852" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
call_tool(self, name: str, arguments: dict[str, Any] | None = None, timeout: datetime.timedelta | float | int | None = None, progress_handler: ProgressHandler | None = None, raise_on_error: bool = True) -> CallToolResult
@ -496,4 +496,10 @@ raw result object.
- `RuntimeError`: If called while the client is not connected.
### `CallToolResult` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L898" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `generate_name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L923" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
generate_name(cls, name: str | None = None) -> str
```
### `CallToolResult` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L932" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -7,8 +7,8 @@ sidebarTitle: sampling
## Functions
### `create_sampling_callback` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/sampling.py#L25" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `create_sampling_callback` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/sampling.py#L31" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_sampling_callback(sampling_handler: SamplingHandler) -> SamplingFnT
create_sampling_callback(sampling_handler: ClientSamplingHandler[LifespanContextT]) -> SamplingFnT
```

View file

@ -7,7 +7,7 @@ sidebarTitle: transports
## Functions
### `infer_transport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L950" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `infer_transport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L971" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
infer_transport(transport: ClientTransport | FastMCP | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str) -> ClientTransport
@ -57,13 +57,13 @@ transport = infer_transport(config)
## Classes
### `SessionKwargs` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L59" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `SessionKwargs` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Keyword arguments for the MCP ClientSession constructor.
### `ClientTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L71" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ClientTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L75" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Abstract base class for different MCP client transport mechanisms.
@ -74,7 +74,7 @@ to an MCP server, and providing a ClientSession within an async context.
**Methods:**
#### `connect_session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L82" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `connect_session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L86" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
@ -93,7 +93,7 @@ within this context.
constructor (e.g., callbacks, timeouts).
#### `close` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L108" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `close` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L112" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
close(self)
@ -102,7 +102,7 @@ close(self)
Close the transport.
### `WSTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L117" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `WSTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L121" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Transport implementation that connects to an MCP server via WebSockets.
@ -110,13 +110,13 @@ Transport implementation that connects to an MCP server via WebSockets.
**Methods:**
#### `connect_session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L135" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `connect_session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L139" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
```
### `SSETransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L156" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `SSETransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L160" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Transport implementation that connects to an MCP server via Server-Sent Events.
@ -124,13 +124,13 @@ Transport implementation that connects to an MCP server via Server-Sent Events.
**Methods:**
#### `connect_session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L192" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `connect_session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L196" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
```
### `StreamableHttpTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L228" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `StreamableHttpTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L230" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Transport implementation that connects to an MCP server via Streamable HTTP Requests.
@ -138,7 +138,7 @@ Transport implementation that connects to an MCP server via Streamable HTTP Requ
**Methods:**
#### `connect_session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L264" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `connect_session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L266" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
@ -179,43 +179,43 @@ disconnect(self)
close(self)
```
### `PythonStdioTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L465" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `PythonStdioTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L463" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Transport for running Python scripts.
### `FastMCPStdioTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L511" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `FastMCPStdioTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L509" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Transport for running FastMCP servers using the FastMCP CLI.
### `NodeStdioTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L538" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `NodeStdioTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L536" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Transport for running Node.js scripts.
### `UvStdioTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L580" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `UvStdioTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L578" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Transport for running commands via the uv tool.
### `UvxStdioTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L637" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `UvxStdioTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L657" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Transport for running commands via the uvx tool.
### `NpxStdioTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L701" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `NpxStdioTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L721" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Transport for running commands via the npx tool.
### `FastMCPTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L763" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `FastMCPTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L783" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
In-memory transport for FastMCP servers.
@ -228,13 +228,13 @@ tests or scenarios where client and server run in the same runtime.
**Methods:**
#### `connect_session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L782" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `connect_session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L802" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
```
### `MCPConfigTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L817" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `MCPConfigTransport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L837" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Transport for connecting to one or more MCP servers defined in an MCPConfig.
@ -287,13 +287,13 @@ async with client:
**Methods:**
#### `connect_session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L898" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `connect_session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L919" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
```
#### `close` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L904" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `close` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/transports.py#L925" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
close(self)

View file

@ -32,7 +32,7 @@ Example configuration:
## Functions
### `infer_transport_type_from_url` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L57" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `infer_transport_type_from_url` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L56" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
infer_transport_type_from_url(url: str | AnyUrl) -> Literal['http', 'sse']
@ -42,7 +42,7 @@ infer_transport_type_from_url(url: str | AnyUrl) -> Literal['http', 'sse']
Infer the appropriate transport type from the given URL.
### `update_config_file` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L300" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `update_config_file` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L313" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
update_config_file(file_path: Path, server_name: str, server_config: CanonicalMCPServerTypes) -> None
@ -57,7 +57,7 @@ worry about transforming server objects here.
## Classes
### `StdioMCPServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L117" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `StdioMCPServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L126" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
MCP server configuration for stdio transport.
@ -67,19 +67,19 @@ This is the canonical configuration format for MCP servers using stdio transport
**Methods:**
#### `to_transport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L147" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `to_transport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L156" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
to_transport(self) -> StdioTransport
```
### `TransformingStdioMCPServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L158" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `TransformingStdioMCPServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L167" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A Stdio server with tool transforms.
### `RemoteMCPServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L162" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `RemoteMCPServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L171" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
MCP server configuration for HTTP/SSE transport.
@ -89,19 +89,19 @@ This is the canonical configuration format for MCP servers using remote transpor
**Methods:**
#### `to_transport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L198" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `to_transport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L207" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
to_transport(self) -> StreamableHttpTransport | SSETransport
```
### `TransformingRemoteMCPServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L223" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `TransformingRemoteMCPServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L232" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A Remote server with tool transforms.
### `MCPConfig` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L234" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `MCPConfig` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L243" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A configuration object for MCP Servers that conforms to the canonical MCP configuration format
@ -113,16 +113,16 @@ For an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class.
**Methods:**
#### `validate_mcp_servers` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L247" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `wrap_servers_at_root` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L257" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
validate_mcp_servers(self, info: ValidationInfo) -> dict[str, Any]
wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any]
```
Validate the MCP servers.
If there's no mcpServers key but there are server configs at root, wrap them.
#### `add_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L257" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L270" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_server(self, name: str, server: MCPServerTypes) -> None
@ -131,7 +131,7 @@ add_server(self, name: str, server: MCPServerTypes) -> None
Add or update a server in the configuration.
#### `from_dict` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L262" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_dict` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L275" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_dict(cls, config: dict[str, Any]) -> Self
@ -140,7 +140,7 @@ from_dict(cls, config: dict[str, Any]) -> Self
Parse MCP configuration from dictionary format.
#### `to_dict` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L266" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `to_dict` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L279" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
to_dict(self) -> dict[str, Any]
@ -149,7 +149,7 @@ to_dict(self) -> dict[str, Any]
Convert MCPConfig to dictionary format, preserving all fields.
#### `write_to_file` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L270" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `write_to_file` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L283" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
write_to_file(self, file_path: Path) -> None
@ -158,7 +158,7 @@ write_to_file(self, file_path: Path) -> None
Write configuration to JSON file.
#### `from_file` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L276" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_file` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L289" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_file(cls, file_path: Path) -> Self
@ -167,7 +167,7 @@ from_file(cls, file_path: Path) -> Self
Load configuration from JSON file.
### `CanonicalMCPConfig` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L285" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `CanonicalMCPConfig` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L298" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Canonical MCP configuration format.
@ -178,7 +178,7 @@ The format is designed to be client-agnostic and extensible for future use cases
**Methods:**
#### `add_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L295" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L308" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_server(self, name: str, server: CanonicalMCPServerTypes) -> None

View file

@ -26,10 +26,10 @@ production use with enterprise identity providers.
## Classes
### `ProxyDCRClient` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ProxyDCRClient` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L55" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Client for DCR proxy that accepts any localhost redirect URI.
Client for DCR proxy with configurable redirect URI validation.
This special client class is critical for the OAuth proxy to work correctly
with Dynamic Client Registration (DCR). Here's why it exists:
@ -38,39 +38,39 @@ Problem:
--------
When MCP clients use OAuth, they dynamically register with random localhost
ports (e.g., http://localhost:55454/callback). The OAuth proxy needs to:
1. Accept these dynamic redirect URIs from clients
1. Accept these dynamic redirect URIs from clients based on configured patterns
2. Use its own fixed redirect URI with the upstream provider (Google, GitHub, etc.)
3. Forward the authorization code back to the client's dynamic URI
Solution:
---------
This class overrides redirect_uri validation to accept ANY localhost URI,
This class validates redirect URIs against configurable patterns,
while the proxy internally uses its own fixed redirect URI with the upstream
provider. This allows the flow to work even when clients reconnect with
different ports or when tokens are cached.
Without this class, clients would get "Redirect URI not registered" errors
when trying to authenticate with cached tokens, because the stored client
would have fixed redirect URIs that don't match the new dynamic port.
Without proper validation, clients could get "Redirect URI not registered" errors
when trying to authenticate with cached tokens, or security vulnerabilities could
arise from accepting arbitrary redirect URIs.
**Methods:**
#### `validate_redirect_uri` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L80" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `validate_redirect_uri` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L94" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl
```
Accept any localhost redirect URI for DCR clients.
Validate redirect URI against allowed patterns.
Since we're acting as a proxy and clients register dynamically,
we need to accept their localhost redirect URIs even though they're
not pre-registered with us. This is essential for cached token
scenarios where the client may reconnect with a different port.
we validate their redirect URIs against configurable patterns.
This is essential for cached token scenarios where the client may
reconnect with a different port.
### `OAuthProxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L107" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `OAuthProxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L120" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
OAuth provider that presents a DCR-compliant interface while proxying to non-DCR IDPs.
@ -182,7 +182,7 @@ Handles provider-specific requirements:
**Methods:**
#### `get_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L333" 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#L354" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_client(self, client_id: str) -> OAuthClientInformationFull | None
@ -199,7 +199,7 @@ handles the case where a client with cached tokens reconnects
on a different port.
#### `register_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L364" 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#L386" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
register_client(self, client_info: OAuthClientInformationFull) -> None
@ -226,7 +226,7 @@ The flow:
4. When client reconnects with a different port, ProxyDCRClient accepts it
#### `authorize` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L421" 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#L444" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
@ -240,7 +240,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#L478" 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#L501" 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 +252,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#L520" 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#L543" 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 +264,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#L587" 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#L610" 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 +273,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#L595" 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#L618" 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 +282,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#L670" 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#L693" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
load_access_token(self, token: str) -> AccessToken | None
@ -294,7 +294,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#L687" 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#L710" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
revoke_token(self, token: AccessToken | RefreshToken) -> None
@ -306,7 +306,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#L880" 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#L903" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_routes(self) -> list[Route]

View file

@ -69,23 +69,26 @@ Settings for JWT token verification.
### `JWTVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L160" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
JWT token verifier using public key or JWKS.
JWT token verifier supporting both asymmetric (RSA/ECDSA) and symmetric (HMAC) algorithms.
This verifier validates JWT tokens signed by an external issuer. It's ideal for
scenarios where you have a centralized identity provider (like Auth0, Okta, or
your own OAuth server) that issues JWTs, and your FastMCP server acts as a
resource server validating those tokens.
This verifier validates JWT tokens using various signing algorithms:
- **Asymmetric algorithms** (RS256/384/512, ES256/384/512, PS256/384/512):
Uses public/private key pairs. Ideal for external clients and services where
only the authorization server has the private key.
- **Symmetric algorithms** (HS256/384/512): Uses a shared secret for both
signing and verification. Perfect for internal microservices and trusted
environments where the secret can be securely shared.
Use this when:
- You have JWT tokens issued by an external service
- You want asymmetric key verification (public/private key pairs)
- You need JWKS support for automatic key rotation
- You have JWT tokens issued by an external service (asymmetric)
- You need JWKS support for automatic key rotation (asymmetric)
- You have internal microservices sharing a secret key (symmetric)
- Your tokens contain standard OAuth scopes and claims
**Methods:**
#### `load_access_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L356" 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/providers/jwt.py#L362" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
load_access_token(self, token: str) -> AccessToken | None
@ -100,7 +103,7 @@ Validates the provided JWT bearer token.
- AccessToken object if valid, None if invalid or expired
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L458" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L464" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
verify_token(self, token: str) -> AccessToken | None
@ -118,7 +121,7 @@ to our existing load_access_token method.
- AccessToken object if valid, None if invalid or expired
### `StaticTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L474" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `StaticTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L480" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Simple static token verifier for testing and development.
@ -139,7 +142,7 @@ WARNING: Never use this in production - tokens are stored in plain text!
**Methods:**
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L508" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/jwt.py#L514" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
verify_token(self, token: str) -> AccessToken | None

View file

@ -0,0 +1,52 @@
---
title: redirect_validation
sidebarTitle: redirect_validation
---
# `fastmcp.server.auth.redirect_validation`
Utilities for validating client redirect URIs in OAuth flows.
## Functions
### `matches_allowed_pattern` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/redirect_validation.py#L8" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
matches_allowed_pattern(uri: str, pattern: str) -> bool
```
Check if a URI matches an allowed pattern with wildcard support.
Patterns support * wildcard matching:
- http://localhost:* matches any localhost port
- http://127.0.0.1:* matches any 127.0.0.1 port
- https://*.example.com/* matches any subdomain of example.com
- https://app.example.com/auth/* matches any path under /auth/
**Args:**
- `uri`: The redirect URI to validate
- `pattern`: The allowed pattern (may contain wildcards)
**Returns:**
- True if the URI matches the pattern
### `validate_redirect_uri` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/redirect_validation.py#L28" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
validate_redirect_uri(redirect_uri: str | AnyUrl | None, allowed_patterns: list[str] | None) -> bool
```
Validate a redirect URI against allowed patterns.
**Args:**
- `redirect_uri`: The redirect URI to validate
- `allowed_patterns`: List of allowed patterns. If None, defaults to localhost.
If empty list, all URIs are allowed.
**Returns:**
- True if the redirect URI is allowed

View file

@ -7,7 +7,7 @@ sidebarTitle: context
## Functions
### `set_context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L64" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `set_context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L68" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_context(context: Context) -> Generator[Context, None, None]
@ -15,7 +15,7 @@ set_context(context: Context) -> Generator[Context, None, None]
## Classes
### `LogData` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L52" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `LogData` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L56" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Data object for passing log arguments to client-side handlers.
@ -24,7 +24,7 @@ This provides an interface to match the Python standard library logging,
for compatibility with structured logging.
### `Context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L73" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `Context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L77" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Context object providing access to MCP capabilities.
@ -72,7 +72,7 @@ The context is optional - tools that don't need it can omit the parameter.
**Methods:**
#### `fastmcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L125" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `fastmcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L129" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
fastmcp(self) -> FastMCP
@ -81,7 +81,7 @@ fastmcp(self) -> FastMCP
Get the FastMCP instance.
#### `request_context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L154" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `request_context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L158" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
request_context(self) -> RequestContext[ServerSession, Any, Request]
@ -92,7 +92,7 @@ Access to the underlying request context.
If called outside of a request context, this will raise a ValueError.
#### `report_progress` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L164" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `report_progress` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L168" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None
@ -105,7 +105,7 @@ Report progress for the current operation.
- `total`: Optional total value e.g. 100
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L191" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L195" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]
@ -120,7 +120,7 @@ Read a resource by URI.
- The resource content as either text or bytes
#### `log` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L204" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `log` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L208" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
log(self, message: str, level: LoggingLevel | None = None, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@ -136,7 +136,7 @@ Send a log message to the client.
- `extra`: Optional mapping for additional arguments
#### `client_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L231" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `client_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L235" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
client_id(self) -> str | None
@ -145,7 +145,7 @@ client_id(self) -> str | None
Get the client ID if available.
#### `request_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L240" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `request_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L244" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
request_id(self) -> str
@ -154,7 +154,7 @@ request_id(self) -> str
Get the unique ID for this request.
#### `session_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L245" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `session_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L249" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
session_id(self) -> str
@ -171,7 +171,7 @@ the same client session.
- for other transports.
#### `session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L289" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L293" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
session(self) -> ServerSession
@ -180,7 +180,7 @@ session(self) -> ServerSession
Access to the underlying session for advanced usage.
#### `debug` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L294" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `debug` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L298" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
debug(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@ -189,7 +189,7 @@ debug(self, message: str, logger_name: str | None = None, extra: Mapping[str, An
Send a debug log message.
#### `info` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L305" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `info` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L309" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
info(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@ -198,7 +198,7 @@ info(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any
Send an info log message.
#### `warning` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L316" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `warning` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L320" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
warning(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@ -207,7 +207,7 @@ warning(self, message: str, logger_name: str | None = None, extra: Mapping[str,
Send a warning log message.
#### `error` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L327" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `error` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L331" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
error(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@ -216,7 +216,7 @@ error(self, message: str, logger_name: str | None = None, extra: Mapping[str, An
Send an error log message.
#### `list_roots` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L338" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_roots` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L342" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_roots(self) -> list[Root]
@ -225,7 +225,7 @@ list_roots(self) -> list[Root]
List the roots available to the server, as indicated by the client.
#### `send_tool_list_changed` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L343" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `send_tool_list_changed` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L347" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
send_tool_list_changed(self) -> None
@ -234,7 +234,7 @@ send_tool_list_changed(self) -> None
Send a tool list changed notification to the client.
#### `send_resource_list_changed` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L347" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `send_resource_list_changed` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L351" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
send_resource_list_changed(self) -> None
@ -243,7 +243,7 @@ send_resource_list_changed(self) -> None
Send a resource list changed notification to the client.
#### `send_prompt_list_changed` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L351" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `send_prompt_list_changed` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L355" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
send_prompt_list_changed(self) -> None
@ -252,7 +252,7 @@ send_prompt_list_changed(self) -> None
Send a prompt list changed notification to the client.
#### `sample` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L355" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `sample` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L359" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
sample(self, messages: str | list[str | SamplingMessage], system_prompt: str | None = None, include_context: IncludeContext | None = None, temperature: float | None = None, max_tokens: int | None = None, model_preferences: ModelPreferences | str | list[str] | None = None) -> ContentBlock
@ -265,25 +265,25 @@ completion from the client. The client must be appropriately configured,
or the request will error.
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L402" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L443" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: None) -> AcceptedElicitation[dict[str, Any]] | DeclinedElicitation | CancelledElicitation
```
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L414" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L455" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: type[T]) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation
```
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L424" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L465" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: list[str]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation
```
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L433" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L474" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: type[T] | list[str] | None = None) -> AcceptedElicitation[T] | AcceptedElicitation[dict[str, Any]] | AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation
@ -312,7 +312,7 @@ type or dataclass or BaseModel. If it is a primitive type, an
object schema with a single "value" field will be generated.
#### `get_http_request` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L526" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_http_request` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L567" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_http_request(self) -> Request
@ -321,7 +321,7 @@ get_http_request(self) -> Request
Get the active starlette request.
#### `set_state` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L541" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `set_state` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L582" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_state(self, key: str, value: Any) -> None
@ -330,7 +330,7 @@ set_state(self, key: str, value: Any) -> None
Set a value in the context state.
#### `get_state` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L545" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_state` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L586" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_state(self, key: str) -> Any

View file

@ -268,7 +268,7 @@ Supports forwarding roots, sampling, elicitation, logging, and progress.
**Methods:**
#### `default_sampling_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/proxy.py#L562" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `default_sampling_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/proxy.py#L564" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
default_sampling_handler(cls, messages: list[mcp.types.SamplingMessage], params: mcp.types.CreateMessageRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> mcp.types.CreateMessageResult
@ -277,7 +277,7 @@ default_sampling_handler(cls, messages: list[mcp.types.SamplingMessage], params:
A handler that forwards the sampling request from the remote server to the proxy's connected clients and relays the response back to the remote server.
#### `default_elicitation_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/proxy.py#L588" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `default_elicitation_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/proxy.py#L590" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
default_elicitation_handler(cls, message: str, response_type: type, params: mcp.types.ElicitRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> ElicitResult
@ -286,7 +286,7 @@ default_elicitation_handler(cls, message: str, response_type: type, params: mcp.
A handler that forwards the elicitation request from the remote server to the proxy's connected clients and relays the response back to the remote server.
#### `default_log_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/proxy.py#L607" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `default_log_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/proxy.py#L609" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
default_log_handler(cls, message: LogMessage) -> None
@ -295,7 +295,7 @@ default_log_handler(cls, message: LogMessage) -> None
A handler that forwards the log notification from the remote server to the proxy's connected clients.
#### `default_progress_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/proxy.py#L617" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `default_progress_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/proxy.py#L619" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
default_progress_handler(cls, progress: float, total: float | None, message: str | None) -> None
@ -304,7 +304,7 @@ default_progress_handler(cls, progress: float, total: float | None, message: str
A handler that forwards the progress notification from the remote server to the proxy's connected clients.
### `StatefulProxyClient` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/proxy.py#L630" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `StatefulProxyClient` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/proxy.py#L632" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A proxy client that provides a stateful client factory for the proxy server.
@ -318,7 +318,7 @@ Note that it is essential to ensure that the proxy server itself is also statefu
**Methods:**
#### `clear` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/proxy.py#L652" 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/server/proxy.py#L654" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
clear(self)
@ -327,7 +327,7 @@ clear(self)
Clear all cached clients and force disconnect them.
#### `new_stateful` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/proxy.py#L660" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `new_stateful` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/proxy.py#L662" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
new_stateful(self) -> Client[ClientTransportT]

View file

@ -10,7 +10,7 @@ FastMCP - A more ergonomic interface for MCP servers.
## Functions
### `default_lifespan` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L96" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `default_lifespan` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L99" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]
@ -26,7 +26,7 @@ Default lifespan context manager that does nothing.
- An empty context object
### `add_resource_prefix` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2182" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `add_resource_prefix` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2223" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> str
@ -64,7 +64,7 @@ add_resource_prefix("resource:///absolute/path", "prefix")
- `ValueError`: If the URI doesn't match the expected protocol\://path format
### `remove_resource_prefix` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2242" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `remove_resource_prefix` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2283" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
remove_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> str
@ -103,7 +103,7 @@ remove_resource_prefix("resource://prefix//absolute/path", "prefix")
- `ValueError`: If the URI doesn't match the expected protocol\://path format
### `has_resource_prefix` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2309" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `has_resource_prefix` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2350" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
has_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> bool
@ -143,35 +143,35 @@ False
## Classes
### `FastMCP` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L127" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `FastMCP` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L130" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
**Methods:**
#### `settings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L289" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `settings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L315" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
settings(self) -> Settings
```
#### `name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L300" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L326" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
name(self) -> str
```
#### `instructions` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L304" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `instructions` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L330" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
instructions(self) -> str | None
```
#### `version` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L308" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `version` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L334" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
version(self) -> str | None
```
#### `run_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L311" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L337" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run_async(self, transport: Transport | None = None, show_banner: bool = True, **transport_kwargs: Any) -> None
@ -183,7 +183,7 @@ Run the FastMCP server asynchronously.
- `transport`: Transport protocol to use ("stdio", "sse", or "streamable-http")
#### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L341" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L367" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run(self, transport: Transport | None = None, show_banner: bool = True, **transport_kwargs: Any) -> None
@ -195,13 +195,13 @@ Run the FastMCP server. Note this is a synchronous function.
- `transport`: Transport protocol to use ("stdio", "sse", or "streamable-http")
#### `add_middleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L383" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_middleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L409" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_middleware(self, middleware: Middleware) -> None
```
#### `get_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L386" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L412" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tools(self) -> dict[str, Tool]
@ -210,13 +210,13 @@ get_tools(self) -> dict[str, Tool]
Get all registered tools, indexed by registered key.
#### `get_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L390" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L416" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tool(self, key: str) -> Tool
```
#### `get_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L396" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L422" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_resources(self) -> dict[str, Resource]
@ -225,13 +225,13 @@ get_resources(self) -> dict[str, Resource]
Get all registered resources, indexed by registered key.
#### `get_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L400" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L426" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_resource(self, key: str) -> Resource
```
#### `get_resource_templates` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L406" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_resource_templates` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L432" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_resource_templates(self) -> dict[str, ResourceTemplate]
@ -240,7 +240,7 @@ get_resource_templates(self) -> dict[str, ResourceTemplate]
Get all registered resource templates, indexed by registered key.
#### `get_resource_template` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L410" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_resource_template` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L436" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_resource_template(self, key: str) -> ResourceTemplate
@ -249,7 +249,7 @@ get_resource_template(self, key: str) -> ResourceTemplate
Get a registered resource template by key.
#### `get_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L417" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L443" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompts(self) -> dict[str, Prompt]
@ -258,13 +258,13 @@ get_prompts(self) -> dict[str, Prompt]
List all available prompts.
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L423" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L449" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompt(self, key: str) -> Prompt
```
#### `custom_route` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L429" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `custom_route` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L455" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
custom_route(self, path: str, methods: list[str], name: str | None = None, include_in_schema: bool = True) -> Callable[[Callable[[Request], Awaitable[Response]]], Callable[[Request], Awaitable[Response]]]
@ -285,7 +285,7 @@ Starlette's reverse URL lookup feature)
- `include_in_schema`: Whether to include in OpenAPI schema, defaults to True
#### `add_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L827" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L857" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_tool(self, tool: Tool) -> Tool
@ -303,7 +303,7 @@ with the Context type annotation. See the @tool decorator for examples.
- The tool instance that was added to the server.
#### `remove_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L852" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `remove_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L882" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
remove_tool(self, name: str) -> None
@ -318,7 +318,7 @@ Remove a tool from the server.
- `NotFoundError`: If the tool is not found
#### `add_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L872" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L902" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfig) -> None
@ -327,7 +327,7 @@ add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfi
Add a tool transformation.
#### `remove_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L878" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `remove_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L908" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
remove_tool_transformation(self, tool_name: str) -> None
@ -336,19 +336,19 @@ remove_tool_transformation(self, tool_name: str) -> None
Remove a tool transformation.
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L883" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L913" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: AnyFunction) -> FunctionTool
```
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L899" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L929" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionTool]
```
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L914" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L944" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool
@ -404,7 +404,7 @@ server.tool(my_function, name="custom_name")
```
#### `add_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1045" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1075" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_resource(self, resource: Resource) -> Resource
@ -419,7 +419,7 @@ Add a resource to the server.
- The resource instance that was added to the server.
#### `add_template` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1067" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_template` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1097" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_template(self, template: ResourceTemplate) -> ResourceTemplate
@ -434,7 +434,7 @@ Add a resource template to the server.
- The template instance that was added to the server.
#### `add_resource_fn` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1089" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_resource_fn` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1119" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_resource_fn(self, fn: AnyFunction, uri: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None) -> None
@ -454,7 +454,7 @@ has parameters, it will be registered as a template resource.
- `tags`: Optional set of tags for categorizing the resource
#### `resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1127" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1157" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
resource(self, uri: str) -> Callable[[AnyFunction], Resource | ResourceTemplate]
@ -514,7 +514,7 @@ async def get_weather(city: str) -> str:
```
#### `add_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1264" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1294" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_prompt(self, prompt: Prompt) -> Prompt
@ -529,19 +529,19 @@ Add a prompt to the server.
- The prompt instance that was added to the server.
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1287" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1317" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
prompt(self, name_or_fn: AnyFunction) -> FunctionPrompt
```
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1300" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1330" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
prompt(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionPrompt]
```
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1312" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1342" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
prompt(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt
@ -619,7 +619,7 @@ Decorator to register a prompt.
```
#### `run_stdio_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1453" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run_stdio_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1483" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run_stdio_async(self, show_banner: bool = True) -> None
@ -628,7 +628,7 @@ run_stdio_async(self, show_banner: bool = True) -> None
Run the server using stdio transport.
#### `run_http_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1473" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run_http_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1503" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run_http_async(self, show_banner: bool = True, transport: Literal['http', 'streamable-http', 'sse'] = 'http', host: str | None = None, port: int | None = None, log_level: str | None = None, path: str | None = None, uvicorn_config: dict[str, Any] | None = None, middleware: list[ASGIMiddleware] | None = None, stateless_http: bool | None = None) -> None
@ -647,7 +647,7 @@ Run the server using HTTP transport.
- `stateless_http`: Whether to use stateless HTTP (defaults to settings.stateless_http)
#### `run_sse_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1547" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run_sse_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1577" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run_sse_async(self, host: str | None = None, port: int | None = None, log_level: str | None = None, path: str | None = None, uvicorn_config: dict[str, Any] | None = None) -> None
@ -656,7 +656,7 @@ run_sse_async(self, host: str | None = None, port: int | None = None, log_level:
Run the server using SSE transport.
#### `sse_app` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1575" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `sse_app` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1605" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
sse_app(self, path: str | None = None, message_path: str | None = None, middleware: list[ASGIMiddleware] | None = None) -> StarletteWithLifespan
@ -670,7 +670,7 @@ Create a Starlette app for the SSE server.
- `middleware`: A list of middleware to apply to the app
#### `streamable_http_app` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1606" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `streamable_http_app` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1636" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
streamable_http_app(self, path: str | None = None, middleware: list[ASGIMiddleware] | None = None) -> StarletteWithLifespan
@ -683,7 +683,7 @@ Create a Starlette app for the StreamableHTTP server.
- `middleware`: A list of middleware to apply to the app
#### `http_app` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1627" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `http_app` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1657" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
http_app(self, path: str | None = None, middleware: list[ASGIMiddleware] | None = None, json_response: bool | None = None, stateless_http: bool | None = None, transport: Literal['http', 'streamable-http', 'sse'] = 'http') -> StarletteWithLifespan
@ -700,13 +700,13 @@ Create a Starlette app using the specified HTTP transport.
- A Starlette application configured with the specified transport
#### `run_streamable_http_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1676" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run_streamable_http_async` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1706" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run_streamable_http_async(self, host: str | None = None, port: int | None = None, log_level: str | None = None, path: str | None = None, uvicorn_config: dict[str, Any] | None = None) -> None
```
#### `mount` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1701" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `mount` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1731" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
mount(self, server: FastMCP[LifespanResultT], prefix: str | None = None, as_proxy: bool | None = None) -> None
@ -760,7 +760,7 @@ automatically determined based on whether the server has a custom lifespan
- `prompt_separator`: Deprecated. Separator character for prompt names.
#### `import_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1823" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `import_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1853" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
import_server(self, server: FastMCP[LifespanResultT], prefix: str | None = None, tool_separator: str | None = None, resource_separator: str | None = None, prompt_separator: str | None = None) -> None
@ -801,7 +801,7 @@ applied using the protocol\://prefix/path format
- `prompt_separator`: Deprecated. Separator for prompt names.
#### `from_openapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1950" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_openapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1982" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, route_maps: list[RouteMap] | list[RouteMapNew] | None = None, route_map_fn: OpenAPIRouteMapFn | OpenAPIRouteMapFnNew | None = None, mcp_component_fn: OpenAPIComponentFn | OpenAPIComponentFnNew | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, **settings: Any) -> FastMCPOpenAPI | FastMCPOpenAPINew
@ -810,7 +810,7 @@ from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, route
Create a FastMCP server from an OpenAPI specification.
#### `from_fastapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1999" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_fastapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2031" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap] | list[RouteMapNew] | None = None, route_map_fn: OpenAPIRouteMapFn | OpenAPIRouteMapFnNew | None = None, mcp_component_fn: OpenAPIComponentFn | OpenAPIComponentFnNew | None = None, mcp_names: dict[str, str] | None = None, httpx_client_kwargs: dict[str, Any] | None = None, tags: set[str] | None = None, **settings: Any) -> FastMCPOpenAPI | FastMCPOpenAPINew
@ -819,7 +819,7 @@ from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap]
Create a FastMCP server from a FastAPI application.
#### `as_proxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2062" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `as_proxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2094" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
@ -833,7 +833,7 @@ instance or any value accepted as the `transport` argument of
`fastmcp.client.Client` constructor.
#### `from_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2123" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2155" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_client(cls, client: Client[ClientTransportT], **settings: Any) -> FastMCPProxy
@ -842,4 +842,10 @@ from_client(cls, client: Client[ClientTransportT], **settings: Any) -> FastMCPPr
Create a FastMCP proxy server from a FastMCP client.
### `MountedServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2176" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `generate_name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2207" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
generate_name(cls, name: str | None = None) -> str
```
### `MountedServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2217" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -71,7 +71,7 @@ This property is for backwards compatibility with FastMCP < 2.8.0,
which accessed fastmcp.settings.settings
#### `normalize_log_level` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L153" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `normalize_log_level` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L154" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
normalize_log_level(cls, v)

View file

@ -260,7 +260,7 @@ Tool.from_tool(parent, output_schema={
})
# Disable structured outputs
Tool.from_tool(parent, output_schema=False)
Tool.from_tool(parent, output_schema=None)
# Return ToolResult for full control
async def custom_output(**kwargs) -> ToolResult:

View file

@ -7,7 +7,7 @@ sidebarTitle: cli
## Functions
### `log_server_banner` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/cli.py#L26" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `log_server_banner` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/cli.py#L27" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
log_server_banner(server: FastMCP[Any], transport: Literal['stdio', 'http', 'sse', 'streamable-http']) -> None

View file

@ -0,0 +1,13 @@
---
title: fastmcp_config
sidebarTitle: fastmcp_config
---
# `fastmcp.utilities.fastmcp_config`
FastMCP Configuration module.
This module provides versioned configuration support for FastMCP servers.
The current version is v1, which is re-exported here for convenience.

View file

@ -7,7 +7,7 @@ sidebarTitle: mcp_config
## Functions
### `mcp_config_to_servers_and_transports` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_config.py#L11" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `mcp_config_to_servers_and_transports` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_config.py#L17" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
mcp_config_to_servers_and_transports(config: MCPConfig) -> list[tuple[str, FastMCP[Any], ClientTransport]]
@ -17,7 +17,7 @@ mcp_config_to_servers_and_transports(config: MCPConfig) -> list[tuple[str, FastM
A utility function to convert each entry of an MCP Config into a transport and server.
### `mcp_server_type_to_servers_and_transports` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_config.py#L21" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `mcp_server_type_to_servers_and_transports` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_config.py#L27" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
mcp_server_type_to_servers_and_transports(name: str, mcp_server: MCPServerTypes) -> tuple[str, FastMCP[Any], ClientTransport]

View file

@ -10,7 +10,7 @@ Common types used across FastMCP.
## Functions
### `get_cached_typeadapter` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L39" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `get_cached_typeadapter` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L41" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_cached_typeadapter(cls: T) -> TypeAdapter[T]
@ -23,7 +23,7 @@ However, this isn't feasible for user-generated functions. Instead, we use a
cache to minimize the cost of creating them as much as possible.
### `issubclass_safe` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L114" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `issubclass_safe` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L116" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
issubclass_safe(cls: type, base: type) -> bool
@ -33,10 +33,10 @@ issubclass_safe(cls: type, base: type) -> bool
Check if cls is a subclass of base, even if cls is a type variable.
### `is_class_member_of_type` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L124" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `is_class_member_of_type` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L126" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
is_class_member_of_type(cls: type, base: type) -> bool
is_class_member_of_type(cls: Any, base: type) -> bool
```
@ -46,7 +46,7 @@ Base can be a type, a UnionType, or an Annotated type. Generic types are not
considered members (e.g. T is not a member of list\[T]).
### `find_kwarg_by_type` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L146" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `find_kwarg_by_type` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L148" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
find_kwarg_by_type(fn: Callable, kwarg_type: type) -> str | None
@ -58,7 +58,7 @@ Find the name of the kwarg that is of type kwarg_type.
Includes union types that contain the kwarg_type, as well as Annotated types.
### `replace_type` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L375" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `replace_type` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L377" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
replace_type(type_, type_map: dict[type, type])
@ -87,13 +87,13 @@ list[list[str]]
## Classes
### `FastMCPBaseModel` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L32" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `FastMCPBaseModel` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Base model for FastMCP models.
### `Image` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L172" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `Image` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L174" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Helper class for returning images from tools.
@ -101,7 +101,7 @@ Helper class for returning images from tools.
**Methods:**
#### `to_image_content` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L209" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `to_image_content` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L211" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
to_image_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp.types.ImageContent
@ -110,7 +110,7 @@ to_image_content(self, mime_type: str | None = None, annotations: Annotations |
Convert to MCP ImageContent.
### `Audio` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L231" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `Audio` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L233" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Helper class for returning audio from tools.
@ -118,13 +118,13 @@ Helper class for returning audio from tools.
**Methods:**
#### `to_audio_content` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L268" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `to_audio_content` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L270" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
to_audio_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp.types.AudioContent
```
### `File` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L289" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `File` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L291" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Helper class for returning audio from tools.
@ -132,8 +132,10 @@ Helper class for returning audio from tools.
**Methods:**
#### `to_resource_content` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L328" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `to_resource_content` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L330" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
to_resource_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp.types.EmbeddedResource
```
### `ContextSamplingFallbackProtocol` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/types.py#L414" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>

View file

@ -1 +0,0 @@
v1.json

View file

@ -1 +0,0 @@
../../../src/fastmcp/utilities/fastmcp_config/v1/schema.json

View file

@ -161,6 +161,15 @@ The `OAuthProxy` class provides the complete proxy implementation:
<ParamField body="resource_server_url" type="AnyHttpUrl | str | None">
Resource server URL (defaults to base_url)
</ParamField>
<ParamField body="allowed_client_redirect_uris" type="list[str] | None">
List of allowed redirect URI patterns for MCP clients. Patterns support wildcards (e.g., `"http://localhost:*"`, `"https://*.example.com/*"`).
- `None` (default): Only localhost redirect URIs allowed (`http://localhost:*`, `http://127.0.0.1:*`)
- Empty list `[]`: All redirect URIs allowed (not recommended for production)
- Custom list: Only matching patterns allowed
These patterns apply to MCP client loopback redirects, NOT the upstream OAuth app redirect URI.
</ParamField>
</Card>
```python
@ -219,6 +228,32 @@ The proxy automatically:
- Validates tokens using your provider's public keys or API
- Maintains PKCE security throughout the flow
## Client Redirect URI Security
<Warning>
By default, OAuth Proxy only accepts localhost redirect URIs from MCP clients for security. You can customize this with the `allowed_client_redirect_uris` parameter:
```python
# Default: localhost only (secure)
auth = OAuthProxy(...)
# Custom patterns with wildcards
auth = OAuthProxy(
...,
allowed_client_redirect_uris=[
"http://localhost:*",
"https://app.example.com/auth/*"
]
)
# Allow all (NOT recommended for production)
auth = OAuthProxy(
...,
allowed_client_redirect_uris=[]
)
```
</Warning>
## Client Compatibility
<Tip>

View file

@ -111,7 +111,9 @@ token_verifier = JWTVerifier(
auth = RemoteAuthProvider(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl("https://auth.yourcompany.com")],
resource_server_url="https://api.yourcompany.com"
resource_server_url="https://api.yourcompany.com",
# Optional: customize allowed client redirect URIs (defaults to localhost only)
allowed_client_redirect_uris=["http://localhost:*", "http://127.0.0.1:*"]
)
mcp = FastMCP(name="Company API", auth=auth)
@ -192,6 +194,18 @@ WorkOS's support for Dynamic Client Registration makes it particularly well-suit
→ **Complete WorkOS tutorial**: [AuthKit Integration Guide](/integrations/authkit)
## Client Redirect URI Security
<Note>
`RemoteAuthProvider` also supports the `allowed_client_redirect_uris` parameter for controlling which redirect URIs are accepted from MCP clients during DCR:
- `None` (default): Only localhost patterns allowed
- Custom list: Specify allowed patterns with wildcard support
- Empty list `[]`: Allow all (not recommended)
This provides defense-in-depth even though DCR providers typically validate redirect URIs themselves.
</Note>
## Implementation Considerations
Remote OAuth integration requires careful attention to several technical details that affect reliability and security.

View file

@ -80,9 +80,48 @@ This configuration creates a server that validates JWTs issued by `auth.yourcomp
The `issuer` parameter ensures tokens come from your trusted authentication system, while `audience` validation prevents tokens intended for other services from being accepted by your MCP server.
#### Symmetric Key Verification (HMAC)
Symmetric key verification uses a shared secret for both signing and validation, making it ideal for internal microservices and trusted environments where the same secret can be securely distributed to both token issuers and validators.
This approach is commonly used in microservices architectures where services share a secret key, or when your authentication service and MCP server are both managed by the same organization. The HMAC algorithms (HS256, HS384, HS512) provide strong security when the shared secret is properly managed.
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.jwt import JWTVerifier
# Use a shared secret for symmetric key verification
verifier = JWTVerifier(
public_key="your-shared-secret-key-minimum-32-chars", # Despite the name, this accepts symmetric secrets
issuer="internal-auth-service",
audience="mcp-internal-api",
algorithm="HS256" # or HS384, HS512 for stronger security
)
mcp = FastMCP(name="Internal API", auth=verifier)
```
The verifier will validate tokens signed with the same secret using the specified HMAC algorithm. This approach offers several advantages for internal systems:
- **Simplicity**: No key pair management or certificate distribution
- **Performance**: HMAC operations are typically faster than RSA
- **Compatibility**: Works well with existing microservice authentication patterns
<Note>
The parameter is named `public_key` for backwards compatibility, but when using HMAC algorithms (HS256/384/512), it accepts the symmetric secret string.
</Note>
<Warning>
**Security Considerations for Symmetric Keys:**
- Use a strong, randomly generated secret (minimum 32 characters recommended)
- Never expose the secret in logs, error messages, or version control
- Implement secure key distribution and rotation mechanisms
- Consider using asymmetric keys (RSA/ECDSA) for external-facing APIs
</Warning>
#### Static Public Key Verification
Static public key verification works when you have a fixed signing key and don't need automatic key rotation. This approach simplifies deployment in environments where JWKS endpoints aren't available.
Static public key verification works when you have a fixed RSA or ECDSA signing key and don't need automatic key rotation. This approach is primarily useful for development environments or controlled deployments where JWKS endpoints aren't available.
```python
from fastmcp import FastMCP
@ -102,7 +141,7 @@ verifier = JWTVerifier(
mcp = FastMCP(name="Protected API", auth=verifier)
```
This configuration validates tokens using a specific public key. The key must correspond to the private key used by your token issuer. While less flexible than JWKS endpoints, this approach works well for controlled environments or when using dedicated signing keys.
This configuration validates tokens using a specific RSA or ECDSA public key. The key must correspond to the private key used by your token issuer. While less flexible than JWKS endpoints, this approach can be useful in development environments or when testing with fixed keys.
### Development and Testing
@ -180,11 +219,17 @@ Environment-based configuration separates authentication settings from applicati
# Enable JWT verification
export FASTMCP_SERVER_AUTH=JWT
# Configure JWT verification parameters
# For asymmetric verification with JWKS endpoint:
export FASTMCP_SERVER_AUTH_JWT_JWKS_URI="https://auth.company.com/.well-known/jwks.json"
export FASTMCP_SERVER_AUTH_JWT_ISSUER="https://auth.company.com"
export FASTMCP_SERVER_AUTH_JWT_AUDIENCE="mcp-production-api"
export FASTMCP_SERVER_AUTH_JWT_REQUIRED_SCOPES="read:data,write:data"
# OR for symmetric key verification (HMAC):
export FASTMCP_SERVER_AUTH_JWT_PUBLIC_KEY="your-shared-secret-key-minimum-32-chars"
export FASTMCP_SERVER_AUTH_JWT_ALGORITHM="HS256" # or HS384, HS512
export FASTMCP_SERVER_AUTH_JWT_ISSUER="internal-auth-service"
export FASTMCP_SERVER_AUTH_JWT_AUDIENCE="mcp-internal-api"
```
With these environment variables configured, your FastMCP server automatically enables JWT verification:

View file

@ -1,9 +1,11 @@
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": "src/atproto_mcp/server.py",
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "src/atproto_mcp/server.py"
},
"environment": {
"dependencies": [
"atproto_mcp@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/atproto_mcp"
]
}
}
}

View file

@ -1,5 +1,5 @@
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": "src/server.py:app",
"environment": {
"python": "3.12",
@ -17,4 +17,4 @@
"FEATURE_FLAGS": "${FEATURE_FLAGS}"
}
}
}
}

View file

@ -1,6 +1,8 @@
{
"$schema": "https://gofastmcp.com/schemas/fastmcp/v1.json",
"entrypoint": "server.py",
"source": {
"path": "server.py"
},
"environment": {
"python": "3.12",
"dependencies": ["requests"]

View file

@ -1,15 +1,12 @@
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": {
"file": "server.py",
"object": "mcp"
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
"python": "3.12",
"dependencies": [
"requests>=2.31.0",
"httpx"
],
"dependencies": ["requests>=2.31.0", "httpx"],
"requirements": null,
"project": null,
"editable": null
@ -27,4 +24,4 @@
"cwd": null,
"args": null
}
}
}

View file

@ -1,15 +1,14 @@
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": "server.py",
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
"path": "server.py"
},
"environment": {
"python": "3.11",
"dependencies": [
"pyautogui",
"Pillow"
]
"dependencies": ["pyautogui", "Pillow"]
},
"deployment": {
"transport": "stdio",
"log_level": "INFO"
}
}
}

View file

@ -1,12 +1,7 @@
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": "memory.py",
"environment": {
"dependencies": [
"pydantic-ai-slim[openai]",
"asyncpg",
"numpy",
"pgvector"
]
"dependencies": ["pydantic-ai-slim[openai]", "asyncpg", "numpy", "pgvector"]
}
}
}

View file

@ -1,4 +1,4 @@
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": "mount_example.py"
}
}

View file

@ -1,7 +1,7 @@
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": "screenshot.py",
"environment": {
"dependencies": ["pyautogui", "Pillow"]
}
}
}

View file

@ -1,9 +1,9 @@
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": "src/smart_home/hub.py",
"environment": {
"dependencies": [
"smart_home@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/smart_home"
]
}
}
}

View file

@ -1,9 +1,9 @@
{
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"entrypoint": "src/smart_home/lights/server.py",
"environment": {
"dependencies": [
"smart_home@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/smart_home"
]
}
}
}

View file

@ -45,6 +45,7 @@ dev = [
"copychat>=0.5.2",
"dirty-equals>=0.9.0",
"fastapi>=0.115.12",
"inline-snapshot[dirty-equals]>=0.27.2",
"ipython>=8.12.3",
"pdbpp>=0.10.3",
"pre-commit",
@ -124,7 +125,6 @@ python-version = "3.10"
[tool.ty.rules]
# Rules with too many errors to fix right now (40+ each)
invalid-argument-type = "ignore" # 40 errors
no-matching-overload = "ignore" # 126 errors
unknown-argument = "ignore" # 61 errors
unresolved-attribute = "ignore" # 60 errors

View file

@ -6,10 +6,11 @@ from fastmcp.settings import Settings
from fastmcp.utilities.logging import configure_logging as _configure_logging
settings = Settings()
_configure_logging(
level=settings.log_level,
enable_rich_tracebacks=settings.enable_rich_tracebacks,
)
if settings.log_enabled:
_configure_logging(
level=settings.log_level,
enable_rich_tracebacks=settings.enable_rich_tracebacks,
)
from fastmcp.server.server import FastMCP
from fastmcp.server.context import Context

View file

@ -6,6 +6,7 @@ import sys
from pathlib import Path
from typing import Any
from fastmcp.utilities.fastmcp_config import Environment
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
@ -89,20 +90,20 @@ def update_claude_config(
else:
env_vars = existing_env
# Build uv run command
args = ["run"]
# Collect all packages in a set to deduplicate
packages = {"fastmcp"}
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
deduplicated_packages = None
if with_packages:
packages.update(pkg for pkg in with_packages if pkg)
deduplicated = list(dict.fromkeys(with_packages))
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
if not deduplicated_packages:
deduplicated_packages = None
# Add all packages with --with
for pkg in sorted(packages):
args.extend(["--with", pkg])
if with_editable:
args.extend(["--with-editable", str(with_editable)])
# Build uv run command using Environment.build_uv_args()
env_config = Environment(
dependencies=deduplicated_packages,
editable=str(with_editable) if with_editable else None,
)
args = env_config.build_uv_args()
# Convert file path to absolute before adding to command
# Split off any :object suffix first

View file

@ -2,16 +2,18 @@
import importlib.metadata
import importlib.util
import json
import os
import platform
import subprocess
import sys
from contextlib import contextmanager
from pathlib import Path
from typing import Annotated, Literal
import cyclopts
import pyperclip
from pydantic import TypeAdapter
from pydantic import TypeAdapter, ValidationError
from rich.console import Console
from rich.table import Table
@ -21,6 +23,7 @@ from fastmcp.cli.install import install_app
from fastmcp.server.server import FastMCP
from fastmcp.utilities.inspect import FastMCPInfo, inspect_fastmcp
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import get_cached_typeadapter
logger = get_logger("cli")
console = Console()
@ -57,46 +60,27 @@ def _parse_env_var(env_var: str) -> tuple[str, str]:
return key.strip(), value.strip()
def _build_uv_command(
server_spec: str,
with_editable: Path | None = None,
with_packages: list[str] | None = None,
no_banner: bool = False,
python_version: str | None = None,
with_requirements: Path | None = None,
project: Path | None = None,
) -> list[str]:
"""Build the uv run command that runs a MCP server through mcp run."""
cmd = ["uv", "run"]
@contextmanager
def with_argv(args: list[str] | None):
"""Temporarily replace sys.argv if args provided.
# Add Python version if specified
if python_version:
cmd.extend(["--python", python_version])
This context manager is used at the CLI boundary to inject
server arguments when needed, without mutating sys.argv deep
in the source loading logic.
# Add project if specified
if project:
cmd.extend(["--project", str(project)])
cmd.extend(["--with", "fastmcp"])
if with_editable:
cmd.extend(["--with-editable", str(with_editable)])
if with_packages:
for pkg in with_packages:
if pkg:
cmd.extend(["--with", pkg])
if with_requirements:
cmd.extend(["--with-requirements", str(with_requirements)])
# Add mcp run command
cmd.extend(["fastmcp", "run", server_spec])
if no_banner:
cmd.append("--no-banner")
return cmd
Args are provided without the script name, so we preserve sys.argv[0]
and replace the rest.
"""
if args is not None:
original = sys.argv[:]
try:
# Preserve the script name (sys.argv[0]) and replace the rest
sys.argv = [sys.argv[0]] + args
yield
finally:
sys.argv = original
else:
yield
@app.command
@ -107,7 +91,7 @@ def version(
cyclopts.Parameter(
"--copy",
help="Copy version information to clipboard",
negative=False,
negative="",
),
] = False,
):
@ -153,7 +137,7 @@ async def dev(
cyclopts.Parameter(
"--with",
help="Additional packages to install",
negative=False,
negative="",
),
] = [],
inspector_version: Annotated[
@ -204,12 +188,16 @@ async def dev(
Args:
server_spec: Python file to run, optionally with :object suffix, or None to auto-detect fastmcp.json
"""
from pathlib import Path
from fastmcp.utilities.fastmcp_config import FastMCPConfig
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
config = None
config_path = None
# Auto-detect fastmcp.json if no server_spec provided
if server_spec is None:
from pathlib import Path
from fastmcp.utilities.fastmcp_config import FastMCPConfig
config_path = Path("fastmcp.json")
if not config_path.exists():
# Check if fastmcp.json exists in current directory
@ -222,44 +210,50 @@ async def dev(
"Please specify a server file or create a fastmcp.json configuration."
)
sys.exit(1)
server_spec = str(config_path)
logger.info(f"Using configuration from {config_path}")
# Load the config to get settings
config = FastMCPConfig.from_file(config_path)
entrypoint = config.get_entrypoint(config_path)
# Convert entrypoint to string format for dev command
if entrypoint.object:
server_spec = f"{entrypoint.file}:{entrypoint.object}"
else:
server_spec = entrypoint.file
# Create FastMCPConfig from server_spec
if server_spec.endswith(".json"):
# Load existing config
config = FastMCPConfig.from_file(Path(server_spec))
# Merge environment settings with CLI args (CLI takes precedence)
if config.environment:
merged_env = config.environment.merge_with_cli_args(
python=python,
with_packages=with_packages,
with_requirements=with_requirements,
project=project,
with_editable=with_editable,
python = python or config.environment.python
project = project or (
Path(config.environment.project) if config.environment.project else None
)
python = merged_env["python"]
with_packages = merged_env["with_packages"]
with_requirements = merged_env["with_requirements"]
project = merged_env["project"]
with_editable = merged_env["with_editable"]
with_requirements = with_requirements or (
Path(config.environment.requirements)
if config.environment.requirements
else None
)
with_editable = with_editable or (
Path(config.environment.editable)
if config.environment.editable
else None
)
# Merge packages from both sources
if config.environment.dependencies:
packages = list(config.environment.dependencies)
if with_packages:
packages.extend(with_packages)
with_packages = packages
# Get server port from deployment config if not specified
if config.deployment and config.deployment.port:
server_port = server_port or config.deployment.port
logger.info(f"Using configuration from {config_path}")
file, server_object = run_module.parse_file_path(server_spec)
else:
# Create config from file path
source = FileSystemSource(path=server_spec)
config = FastMCPConfig(source=source)
logger.debug(
"Starting dev server",
extra={
"file": str(file),
"server_object": server_object,
"server_spec": server_spec,
"with_editable": str(with_editable) if with_editable else None,
"with_packages": with_packages,
"ui_port": ui_port,
@ -268,9 +262,8 @@ async def dev(
)
try:
# Import server to get dependencies
# TODO: Remove dependencies handling (deprecated in v2.11.4)
server: FastMCP = await run_module.import_server(file, server_object)
# Load server to check for deprecated dependencies
server: FastMCP = await config.source.load_server()
if server.dependencies:
import warnings
@ -302,15 +295,20 @@ async def dev(
if inspector_version:
inspector_cmd += f"@{inspector_version}"
uv_cmd = _build_uv_command(
server_spec,
with_editable,
with_packages,
no_banner=True,
python_version=python,
with_requirements=with_requirements,
project=project,
# Create Environment object from CLI args
from fastmcp.utilities.fastmcp_config import Environment
env_config = Environment(
python=python,
dependencies=with_packages if with_packages else None,
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
editable=str(with_editable) if with_editable else None,
)
uv_cmd = ["uv"] + env_config.build_uv_args(["fastmcp", "run", server_spec])
# Add --no-banner flag for dev command
uv_cmd.append("--no-banner")
# Run the MCP Inspector command with shell=True on Windows
shell = sys.platform == "win32"
@ -325,7 +323,7 @@ async def dev(
logger.error(
"Dev server failed",
extra={
"file": str(file),
"file": str(server_spec),
"error": str(e),
"returncode": e.returncode,
},
@ -336,7 +334,7 @@ async def dev(
"npx not found. Please ensure Node.js and npm are properly installed "
"and added to your system PATH. You may need to restart your terminal "
"after installation.",
extra={"file": str(file)},
extra={"file": str(server_spec)},
)
sys.exit(1)
@ -385,7 +383,7 @@ async def run(
cyclopts.Parameter(
"--no-banner",
help="Don't show the server banner",
negative=False,
negative="",
),
] = False,
python: Annotated[
@ -400,7 +398,7 @@ async def run(
cyclopts.Parameter(
"--with",
help="Additional packages to install (can be used multiple times)",
negative=False,
negative="",
),
] = [],
project: Annotated[
@ -417,6 +415,22 @@ async def run(
help="Requirements file to install dependencies from",
),
] = None,
skip_env: Annotated[
bool,
cyclopts.Parameter(
"--skip-env",
help="Skip environment setup with uv (use when already in a uv environment)",
negative="",
),
] = False,
skip_source: Annotated[
bool,
cyclopts.Parameter(
"--skip-source",
help="Skip source preparation step (use when source is already prepared)",
negative="",
),
] = False,
) -> None:
"""Run an MCP server or connect to a remote one.
@ -441,6 +455,7 @@ async def run(
config = None
config_path = None
editable = None # Initialize editable variable
# Auto-detect fastmcp.json if no server_spec provided
if server_spec is None:
@ -460,41 +475,69 @@ async def run(
server_spec = str(config_path)
logger.info(f"Using configuration from {config_path}")
# Load config if server_spec is a fastmcp.json file
if server_spec.endswith("fastmcp.json"):
# Load config if server_spec is a .json file
if server_spec.endswith(".json"):
config_path = Path(server_spec)
if config_path.exists():
config = FastMCPConfig.from_file(config_path)
# Try to load as JSON and discriminate between FastMCPConfig and MCPConfig
try:
with open(config_path) as f:
data = json.load(f)
# Merge deployment config with CLI values (CLI takes precedence)
if config.deployment:
merged_deploy = config.deployment.merge_with_cli_args(
transport=transport,
host=host,
port=port,
path=path,
log_level=log_level,
server_args=list(server_args) if server_args else None,
)
transport = merged_deploy["transport"]
host = merged_deploy["host"]
port = merged_deploy["port"]
path = merged_deploy["path"]
log_level = merged_deploy["log_level"]
server_args = merged_deploy["server_args"] or ()
# Check if it's an MCPConfig first (has canonical mcpServers key)
if "mcpServers" in data:
# It's an MCPConfig, we don't process these in the run command
# They should be handled through different code paths
config = None
else:
# Try to parse as FastMCPConfig
try:
adapter = get_cached_typeadapter(FastMCPConfig)
config = adapter.validate_python(data)
# Merge environment config with CLI values (CLI takes precedence)
if config.environment:
merged_env = config.environment.merge_with_cli_args(
python=python,
with_packages=with_packages,
with_requirements=with_requirements,
project=project,
)
python = merged_env["python"]
with_packages = merged_env["with_packages"]
with_requirements = merged_env["with_requirements"]
project = merged_env["project"]
# Merge deployment config with CLI values (CLI takes precedence)
if config.deployment:
transport = transport or config.deployment.transport
host = host or config.deployment.host
port = port or config.deployment.port
path = path or config.deployment.path
log_level = log_level or config.deployment.log_level
server_args = (
tuple(server_args)
if server_args
else tuple(config.deployment.args or ())
)
# Merge environment config with CLI values (CLI takes precedence)
if config.environment:
python = python or config.environment.python
project = project or (
Path(config.environment.project)
if config.environment.project
else None
)
with_requirements = with_requirements or (
Path(config.environment.requirements)
if config.environment.requirements
else None
)
# Extract editable from config (no CLI override for this)
editable = config.environment.editable
# Merge packages from both sources
if config.environment.dependencies:
packages = list(config.environment.dependencies)
if with_packages:
packages.extend(with_packages)
with_packages = packages
except ValidationError:
# Not a valid FastMCPConfig, treat as regular server spec
config = None
except (json.JSONDecodeError, FileNotFoundError):
# Not a valid JSON file, treat as regular server spec
config = None
else:
config = None
logger.debug(
"Running server or client",
extra={
@ -509,8 +552,11 @@ async def run(
)
# Check if we need to use uv run (either from CLI args or config)
needs_uv = python or with_packages or with_requirements or project
if not needs_uv and config and config.environment:
# Skip if --skip-env flag is set (we're already in a uv environment)
needs_uv = not skip_env and (
python or with_packages or with_requirements or project or editable
)
if not skip_env and not needs_uv and config and config.environment:
# Check if config's environment needs uv
needs_uv = config.environment.needs_uv()
@ -529,6 +575,7 @@ async def run(
path=path,
log_level=log_level,
show_banner=not no_banner,
editable=editable,
)
except Exception as e:
logger.error(
@ -551,6 +598,7 @@ async def run(
log_level=log_level,
server_args=list(server_args),
show_banner=not no_banner,
skip_source=skip_source,
)
except Exception as e:
logger.error(
@ -586,7 +634,7 @@ async def inspect(
cyclopts.Parameter(
"--with",
help="Additional packages to install (can be used multiple times)",
negative=False,
negative="",
),
] = [],
project: Annotated[
@ -621,10 +669,10 @@ async def inspect(
Args:
server_spec: Python file to inspect, optionally with :object suffix, or fastmcp.json
"""
# Load configuration if needed
from pathlib import Path
from fastmcp.utilities.fastmcp_config import FastMCPConfig
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
config = None
config_path = None
@ -647,31 +695,53 @@ async def inspect(
server_spec = str(config_path)
logger.info(f"Using configuration from {config_path}")
# Load config if server_spec is a fastmcp.json file
if server_spec.endswith("fastmcp.json"):
# Create FastMCPConfig from server_spec
if server_spec.endswith(".json"):
config_path = Path(server_spec)
if config_path.exists():
config = FastMCPConfig.from_file(config_path)
# Get the actual entrypoint with resolved paths
entrypoint = config.get_entrypoint(config_path)
try:
with open(config_path) as f:
data = json.load(f)
if entrypoint.object:
server_spec = f"{entrypoint.file}:{entrypoint.object}"
else:
server_spec = entrypoint.file
# Check if it's an MCPConfig (has mcpServers key)
if "mcpServers" in data:
# MCPConfig - we don't process these in inspect
logger.error("MCPConfig files are not supported by inspect command")
sys.exit(1)
else:
# It's a FastMCPConfig
config = FastMCPConfig.from_file(config_path)
# Merge environment settings from config with CLI (CLI takes precedence)
if config.environment:
merged_env = config.environment.merge_with_cli_args(
python=python,
with_packages=with_packages,
with_requirements=with_requirements,
project=project,
)
python = merged_env["python"]
with_packages = merged_env["with_packages"]
with_requirements = merged_env["with_requirements"]
project = merged_env["project"]
# Merge environment settings from config with CLI (CLI takes precedence)
if config.environment:
python = python or config.environment.python
project = project or (
Path(config.environment.project)
if config.environment.project
else None
)
with_requirements = with_requirements or (
Path(config.environment.requirements)
if config.environment.requirements
else None
)
# Merge packages from both sources
if config.environment.dependencies:
packages = list(config.environment.dependencies)
if with_packages:
packages.extend(with_packages)
with_packages = packages
except (json.JSONDecodeError, ValidationError) as e:
logger.error(f"Invalid configuration file: {e}")
sys.exit(1)
else:
logger.error(f"Configuration file not found: {config_path}")
sys.exit(1)
else:
# Create config from file path
source = FileSystemSource(path=server_spec)
config = FastMCPConfig(source=source)
# Check if we need to use uv run
needs_uv = python or with_packages or with_requirements or project
@ -680,54 +750,37 @@ async def inspect(
if needs_uv:
# Build and run uv command
if config and config.environment:
# Use environment config's run_with_uv method
inspect_command = [
"fastmcp",
"inspect",
server_spec,
"--output",
str(output),
]
config.environment.run_with_uv(inspect_command)
else:
# Build an EnvironmentConfig from CLI args for consistency
from fastmcp.utilities.fastmcp_config import (
EnvironmentConfig,
)
from fastmcp.utilities.fastmcp_config import Environment
env_config = EnvironmentConfig(
python=python,
dependencies=with_packages,
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
)
# Create or update environment config
env_config = Environment(
python=python,
dependencies=with_packages if with_packages else None,
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
)
inspect_command = [
"fastmcp",
"inspect",
server_spec,
"--output",
str(output),
]
env_config.run_with_uv(inspect_command)
# Direct import path (no uv needed)
# Parse the server specification
file, server_object = run_module.parse_file_path(server_spec)
inspect_command = [
"fastmcp",
"inspect",
server_spec,
"--output",
str(output),
]
env_config.run_with_uv(inspect_command)
return # run_with_uv exits the process
logger.debug(
"Inspecting server",
extra={
"file": str(file),
"server_object": server_object,
"server_spec": server_spec,
"output": str(output),
},
)
try:
# Import the server
server = await run_module.import_server(file, server_object)
# Load the server using the config
server = await config.source.load_server()
# Get server information - using native async support
info = await inspect_fastmcp(server)
@ -765,43 +818,6 @@ async def inspect(
sys.exit(1)
@app.command
def generate_schema(
*,
output: Annotated[
Path | None,
cyclopts.Parameter(
name=["--output", "-o"],
help="Output file path for the JSON schema",
),
] = None,
) -> None:
"""Generate JSON schema for fastmcp.json configuration files.
This generates a JSON schema that can be used by IDEs and validators
to provide auto-completion and validation for fastmcp.json files.
Examples:
fastmcp generate-schema
fastmcp generate-schema -o schema.json
"""
import json
from fastmcp.utilities.fastmcp_config import (
generate_schema as gen_schema,
)
schema = gen_schema()
schema_json = json.dumps(schema, indent=2)
if output:
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(schema_json)
logger.info(f"Schema written to {output}")
else:
console.print(schema_json)
# Add install subcommands using proper Cyclopts pattern
app.command(install_app)

View file

@ -9,6 +9,7 @@ from typing import Annotated
import cyclopts
from rich import print
from fastmcp.utilities.fastmcp_config import Environment
from fastmcp.utilities.logging import get_logger
from .shared import process_common_args
@ -106,31 +107,23 @@ def install_claude_code(
)
return False
# Build uv run command
args = ["run"]
# Add Python version if specified
if python_version:
args.extend(["--python", python_version])
# Add project if specified
if project:
args.extend(["--project", str(project)])
# Collect all packages in a set to deduplicate
packages = {"fastmcp"}
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
deduplicated_packages = None
if with_packages:
packages.update(pkg for pkg in with_packages if pkg)
deduplicated = list(dict.fromkeys(with_packages))
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
if not deduplicated_packages:
deduplicated_packages = None
# Add all packages with --with
for pkg in sorted(packages):
args.extend(["--with", pkg])
if with_editable:
args.extend(["--with-editable", str(with_editable)])
if with_requirements:
args.extend(["--with-requirements", str(with_requirements)])
# Build uv run command using Environment.build_uv_args()
env_config = Environment(
python=python_version,
dependencies=deduplicated_packages,
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
editable=str(with_editable) if with_editable else None,
)
args = env_config.build_uv_args()
# Build server spec from parsed components
if server_object:
@ -189,7 +182,7 @@ async def claude_code_command(
cyclopts.Parameter(
"--with",
help="Additional packages to install",
negative=False,
negative="",
),
] = [],
env_vars: Annotated[
@ -197,7 +190,7 @@ async def claude_code_command(
cyclopts.Parameter(
"--env",
help="Environment variables in KEY=VALUE format",
negative=False,
negative="",
),
] = [],
env_file: Annotated[

View file

@ -9,6 +9,7 @@ import cyclopts
from rich import print
from fastmcp.mcp_config import StdioMCPServer, update_config_file
from fastmcp.utilities.fastmcp_config import Environment
from fastmcp.utilities.logging import get_logger
from .shared import process_common_args
@ -72,31 +73,22 @@ def install_claude_desktop(
config_file = config_dir / "claude_desktop_config.json"
# Build uv run command
args = ["run"]
# Add Python version if specified
if python_version:
args.extend(["--python", python_version])
# Add project if specified
if project:
args.extend(["--project", str(project)])
# Collect all packages in a set to deduplicate
packages = {"fastmcp"}
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
deduplicated_packages = None
if with_packages:
packages.update(pkg for pkg in with_packages if pkg)
deduplicated = list(dict.fromkeys(with_packages))
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
if not deduplicated_packages:
deduplicated_packages = None
# Add all packages with --with
for pkg in sorted(packages):
args.extend(["--with", pkg])
if with_editable:
args.extend(["--with-editable", str(with_editable)])
if with_requirements:
args.extend(["--with-requirements", str(with_requirements)])
env_config = Environment(
python=python_version,
dependencies=deduplicated_packages,
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
editable=str(with_editable) if with_editable else None,
)
args = env_config.build_uv_args()
# Build server spec from parsed components
if server_object:
@ -162,7 +154,7 @@ async def claude_desktop_command(
cyclopts.Parameter(
"--with",
help="Additional packages to install",
negative=False,
negative="",
),
] = [],
env_vars: Annotated[
@ -170,7 +162,7 @@ async def claude_desktop_command(
cyclopts.Parameter(
"--env",
help="Environment variables in KEY=VALUE format",
negative=False,
negative="",
),
] = [],
env_file: Annotated[

View file

@ -10,6 +10,7 @@ import cyclopts
from rich import print
from fastmcp.mcp_config import StdioMCPServer, update_config_file
from fastmcp.utilities.fastmcp_config import Environment
from fastmcp.utilities.logging import get_logger
from .shared import process_common_args
@ -106,31 +107,22 @@ def install_cursor_workspace(
config_file = cursor_dir / "mcp.json"
# Build uv run command
args = ["run"]
# Add Python version if specified
if python_version:
args.extend(["--python", python_version])
# Add project if specified
if project:
args.extend(["--project", str(project)])
# Collect all packages in a set to deduplicate
packages = {"fastmcp"}
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
deduplicated_packages = None
if with_packages:
packages.update(pkg for pkg in with_packages if pkg)
deduplicated = list(dict.fromkeys(with_packages))
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
if not deduplicated_packages:
deduplicated_packages = None
# Add all packages with --with
for pkg in sorted(packages):
args.extend(["--with", pkg])
if with_editable:
args.extend(["--with-editable", str(with_editable)])
if with_requirements:
args.extend(["--with-requirements", str(with_requirements)])
env_config = Environment(
python=python_version,
dependencies=deduplicated_packages,
requirements=str(with_requirements.resolve()) if with_requirements else None,
project=str(project.resolve()) if project else None,
editable=str(with_editable.resolve()) if with_editable else None,
)
args = env_config.build_uv_args()
# Build server spec from parsed components
if server_object:
@ -194,31 +186,23 @@ def install_cursor(
Returns:
True if installation was successful, False otherwise
"""
# Build uv run command
args = ["run"]
# Add Python version if specified
if python_version:
args.extend(["--python", python_version])
# Add project if specified
if project:
args.extend(["--project", str(project)])
# Collect all packages in a set to deduplicate
packages = {"fastmcp"}
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
deduplicated_packages = None
if with_packages:
packages.update(pkg for pkg in with_packages if pkg)
deduplicated = list(dict.fromkeys(with_packages))
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
if not deduplicated_packages:
deduplicated_packages = None
# Add all packages with --with
for pkg in sorted(packages):
args.extend(["--with", pkg])
if with_editable:
args.extend(["--with-editable", str(with_editable)])
if with_requirements:
args.extend(["--with-requirements", str(with_requirements)])
env_config = Environment(
python=python_version,
dependencies=deduplicated_packages,
requirements=str(with_requirements.resolve()) if with_requirements else None,
project=str(project.resolve()) if project else None,
editable=str(with_editable.resolve()) if with_editable else None,
)
args = env_config.build_uv_args()
# Build server spec from parsed components
if server_object:
@ -289,7 +273,7 @@ async def cursor_command(
cyclopts.Parameter(
"--with",
help="Additional packages to install",
negative=False,
negative="",
),
] = [],
env_vars: Annotated[
@ -297,7 +281,7 @@ async def cursor_command(
cyclopts.Parameter(
"--env",
help="Environment variables in KEY=VALUE format",
negative=False,
negative="",
),
] = [],
env_file: Annotated[

View file

@ -9,6 +9,7 @@ import cyclopts
import pyperclip
from rich import print
from fastmcp.utilities.fastmcp_config import Environment
from fastmcp.utilities.logging import get_logger
from .shared import process_common_args
@ -47,31 +48,22 @@ def install_mcp_json(
True if generation was successful, False otherwise
"""
try:
# Build uv run command
args = ["run"]
# Add Python version if specified
if python_version:
args.extend(["--python", python_version])
# Add project if specified
if project:
args.extend(["--project", str(project)])
# Collect all packages in a set to deduplicate
packages = {"fastmcp"}
# Deduplicate packages and exclude 'fastmcp' since Environment adds it automatically
deduplicated_packages = None
if with_packages:
packages.update(pkg for pkg in with_packages if pkg)
deduplicated = list(dict.fromkeys(with_packages))
deduplicated_packages = [pkg for pkg in deduplicated if pkg != "fastmcp"]
if not deduplicated_packages:
deduplicated_packages = None
# Add all packages with --with
for pkg in sorted(packages):
args.extend(["--with", pkg])
if with_editable:
args.extend(["--with-editable", str(with_editable)])
if with_requirements:
args.extend(["--with-requirements", str(with_requirements)])
env_config = Environment(
python=python_version,
dependencies=deduplicated_packages,
requirements=str(with_requirements) if with_requirements else None,
project=str(project) if project else None,
editable=str(with_editable) if with_editable else None,
)
args = env_config.build_uv_args()
# Build server spec from parsed components
if server_object:
@ -135,7 +127,7 @@ async def mcp_json_command(
cyclopts.Parameter(
"--with",
help="Additional packages to install",
negative=False,
negative="",
),
] = [],
env_vars: Annotated[
@ -143,7 +135,7 @@ async def mcp_json_command(
cyclopts.Parameter(
"--env",
help="Environment variables in KEY=VALUE format",
negative=False,
negative="",
),
] = [],
env_file: Annotated[
@ -158,7 +150,7 @@ async def mcp_json_command(
cyclopts.Parameter(
"--copy",
help="Copy configuration to clipboard instead of printing to stdout",
negative=False,
negative="",
),
] = False,
python: Annotated[

View file

@ -1,12 +1,15 @@
"""Shared utilities for install commands."""
import json
import sys
from pathlib import Path
from dotenv import dotenv_values
from pydantic import ValidationError
from rich import print
from fastmcp.cli.run import import_server, parse_file_path
from fastmcp.utilities.fastmcp_config import FastMCPConfig
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
@ -34,31 +37,46 @@ async def process_common_args(
Handles both fastmcp.json config files and traditional file.py:object syntax.
"""
# Check if server_spec is a fastmcp.json file
if server_spec.endswith("fastmcp.json") or "fastmcp.json" in Path(server_spec).name:
from fastmcp.utilities.fastmcp_config import FastMCPConfig
# Create FastMCPConfig from server_spec
config = None
if server_spec.endswith(".json"):
config_path = Path(server_spec).resolve()
if not config_path.exists():
print(f"[red]Configuration file not found: {config_path}[/red]")
sys.exit(1)
# Load config and get entrypoint
config = FastMCPConfig.from_file(config_path)
entrypoint = config.get_entrypoint(config_path)
try:
with open(config_path) as f:
data = json.load(f)
# Convert to file and server_object
file = Path(entrypoint.file)
server_object = entrypoint.object
# Check if it's an MCPConfig (has mcpServers key)
if "mcpServers" in data:
# MCPConfig files aren't supported for install
print("[red]MCPConfig files are not supported for installation[/red]")
sys.exit(1)
else:
# It's a FastMCPConfig
config = FastMCPConfig.from_file(config_path)
# Merge packages from config if not overridden
if config.environment and config.environment.dependencies:
# Merge with CLI packages (CLI takes precedence)
config_packages = config.environment.dependencies or []
with_packages = list(set(with_packages + config_packages))
# Merge packages from config if not overridden
if config.environment and config.environment.dependencies:
# Merge with CLI packages (CLI takes precedence)
config_packages = list(config.environment.dependencies) or []
with_packages = list(set(with_packages + config_packages))
except (json.JSONDecodeError, ValidationError) as e:
print(f"[red]Invalid configuration file: {e}[/red]")
sys.exit(1)
else:
# Parse traditional server spec
file, server_object = parse_file_path(server_spec)
# Create config from file path
source = FileSystemSource(path=server_spec)
config = FastMCPConfig(source=source)
# Extract file and server_object from the source
# The FileSystemSource handles parsing path:object syntax
file = Path(config.source.path).resolve()
server_object = (
config.source.entrypoint if hasattr(config.source, "entrypoint") else None
)
logger.debug(
"Installing server",
@ -75,7 +93,7 @@ async def process_common_args(
server = None
if not name:
try:
server = await import_server(file, server_object)
server = await config.source.load_server()
name = server.name
except (ImportError, ModuleNotFoundError) as e:
logger.debug(

View file

@ -1,25 +1,23 @@
"""FastMCP run command implementation with enhanced type hints."""
import importlib.util
import inspect
import json
import re
import subprocess
import sys
from functools import partial
from pathlib import Path
from typing import Any, Literal
from mcp.server.fastmcp import FastMCP as FastMCP1x
from pydantic import ValidationError
from fastmcp.server.server import FastMCP
from fastmcp.utilities.fastmcp_config import (
DeploymentConfig,
EntrypointConfig,
EnvironmentConfig,
Environment,
FastMCPConfig,
)
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import get_cached_typeadapter
logger = get_logger("cli.run")
@ -34,150 +32,6 @@ def is_url(path: str) -> bool:
return bool(url_pattern.match(path))
def parse_file_path(server_spec: str) -> tuple[Path, str | None]:
"""Parse a file path that may include a server object specification.
Args:
server_spec: Path to file, optionally with :object suffix
Returns:
Tuple of (file_path, server_object)
"""
# First check if we have a Windows path (e.g., C:\...)
has_windows_drive = len(server_spec) > 1 and server_spec[1] == ":"
# Split on the last colon, but only if it's not part of the Windows drive letter
# and there's actually another colon in the string after the drive letter
if ":" in (server_spec[2:] if has_windows_drive else server_spec):
file_str, server_object = server_spec.rsplit(":", 1)
else:
file_str, server_object = server_spec, None
# Resolve the file path
file_path = Path(file_str).expanduser().resolve()
if not file_path.exists():
logger.error(f"File not found: {file_path}")
sys.exit(1)
if not file_path.is_file():
logger.error(f"Not a file: {file_path}")
sys.exit(1)
return file_path, server_object
async def import_server(file: Path, server_or_factory: str | None = None) -> Any:
"""Import a MCP server from a file.
Args:
file: Path to the file
server_or_factory: Optional object name in format "module:object" or just "object"
Returns:
The server object (or result of calling a factory function)
"""
# Add parent directory to Python path so imports can be resolved
file_dir = str(file.parent)
if file_dir not in sys.path:
sys.path.insert(0, file_dir)
# Import the module
spec = importlib.util.spec_from_file_location("server_module", file)
if not spec or not spec.loader:
logger.error("Could not load module", extra={"file": str(file)})
sys.exit(1)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# If no object specified, try common server names
if not server_or_factory:
# Look for common server instance names
for name in ["mcp", "server", "app"]:
if hasattr(module, name):
obj = getattr(module, name)
if isinstance(obj, FastMCP | FastMCP1x):
return await _resolve_server_or_factory(obj, file, name)
logger.error(
f"No server object found in {file}. Please either:\n"
"1. Use a standard variable name (mcp, server, or app)\n"
"2. Specify the object name in fastmcp.json or use `file.py:object` syntax as your path.",
extra={"file": str(file)},
)
sys.exit(1)
# Handle module:object syntax
if server_or_factory and ":" in server_or_factory:
module_name, object_name = server_or_factory.split(":", 1)
try:
server_module = importlib.import_module(module_name)
obj = getattr(server_module, object_name, None)
except ImportError:
logger.error(
f"Could not import module '{module_name}'",
extra={"file": str(file)},
)
sys.exit(1)
else:
# Just object name
obj = getattr(module, server_or_factory, None)
if obj is None:
logger.error(
f"Server object '{server_or_factory}' not found",
extra={"file": str(file)},
)
sys.exit(1)
return await _resolve_server_or_factory(obj, file, server_or_factory)
async def _resolve_server_or_factory(obj: Any, file: Path, name: str) -> Any:
"""Resolve a server object or factory function to a server instance.
Args:
obj: The object that might be a server or factory function
file: Path to the file for error messages
name: Name of the object for error messages
Returns:
A server instance
"""
# Check if it's a function or coroutine function
if inspect.isfunction(obj) or inspect.iscoroutinefunction(obj):
logger.debug(f"Found factory function '{name}' in {file}")
try:
if inspect.iscoroutinefunction(obj):
# Async factory function
server = await obj()
else:
# Sync factory function
server = obj()
# Validate the result is a FastMCP server
if not isinstance(server, FastMCP | FastMCP1x):
logger.error(
f"Factory function '{name}' must return a FastMCP server instance, "
f"got {type(server).__name__}",
extra={"file": str(file)},
)
sys.exit(1)
logger.debug(f"Factory function '{name}' created server: {server.name}")
return server
except Exception as e:
logger.error(
f"Failed to call factory function '{name}': {e}",
extra={"file": str(file)},
)
sys.exit(1)
# Not a function, return as-is (should be a server instance)
return obj
def run_with_uv(
server_spec: str,
python_version: str | None = None,
@ -190,6 +44,7 @@ def run_with_uv(
path: str | None = None,
log_level: LogLevelType | None = None,
show_banner: bool = True,
editable: str | None = None,
) -> None:
"""Run a MCP server using uv run subprocess.
@ -206,80 +61,83 @@ def run_with_uv(
log_level: Log level
show_banner: Whether to show the server banner
"""
# Check if server_spec is a fastmcp.json file
if server_spec.endswith("fastmcp.json") or "fastmcp.json" in Path(server_spec).name:
# Check if server_spec is a .json file
if server_spec.endswith(".json"):
config_path = Path(server_spec).resolve() # Get absolute path
if config_path.exists():
# Load config
config = FastMCPConfig.from_file(config_path)
# Try to load as JSON and discriminate between FastMCPConfig and MCPConfig
try:
with open(config_path) as f:
data = json.load(f)
# Get entrypoint with resolved paths
entrypoint = config.get_entrypoint(config_path)
if entrypoint.object:
server_spec = f"{entrypoint.file}:{entrypoint.object}"
else:
server_spec = entrypoint.file
# Check if it's an MCPConfig first (has canonical mcpServers key)
if "mcpServers" in data:
# It's an MCPConfig, we don't process it here - just pass through
pass
else:
# Try to parse as FastMCPConfig
try:
adapter = get_cached_typeadapter(FastMCPConfig)
config: FastMCPConfig = adapter.validate_python(data)
# Merge environment config with CLI args
# Check if environment has any non-None values
if config.environment and any(
getattr(config.environment, field, None) is not None
for field in EnvironmentConfig.model_fields
):
merged_env = config.environment.merge_with_cli_args(
python=python_version,
with_packages=with_packages,
with_requirements=with_requirements,
project=project,
)
python_version = merged_env["python"]
with_packages = merged_env["with_packages"]
with_requirements = merged_env["with_requirements"]
project = merged_env["project"]
# Apply deployment settings
if config.deployment:
config.deployment.apply_runtime_settings(config_path)
# Merge deployment config with CLI args
# Check if deployment has any non-None values
if config.deployment and any(
getattr(config.deployment, field, None) is not None
for field in DeploymentConfig.model_fields
):
merged_deploy = config.deployment.merge_with_cli_args(
transport=transport,
host=host,
port=port,
path=path,
log_level=log_level,
)
transport = merged_deploy["transport"]
host = merged_deploy["host"]
port = merged_deploy["port"]
path = merged_deploy["path"]
log_level = merged_deploy["log_level"]
cmd = ["uv", "run"]
# Merge environment config with CLI args (CLI takes precedence)
if config.environment:
# Use CLI values if provided, otherwise fall back to config
python_version = python_version or config.environment.python
project = project or (
Path(config.environment.project)
if config.environment.project
else None
)
with_requirements = with_requirements or (
Path(config.environment.requirements)
if config.environment.requirements
else None
)
editable = editable or config.environment.editable
# Add Python version if specified
if python_version:
cmd.extend(["--python", python_version])
# Merge packages from both sources
# Only merge if with_packages doesn't already contain them
# (they may have been merged already in CLI)
if config.environment.dependencies and not with_packages:
with_packages = list(config.environment.dependencies)
# Add project if specified
if project:
cmd.extend(["--project", str(project)])
# Merge deployment config with CLI args (CLI takes precedence)
if config.deployment:
transport = transport or config.deployment.transport
host = host or config.deployment.host
port = port or config.deployment.port
path = path or config.deployment.path
log_level = log_level or config.deployment.log_level
except ValidationError:
# Not a valid FastMCPConfig, just pass through
pass
except (json.JSONDecodeError, FileNotFoundError):
# Not a valid JSON file, just pass through
pass
# Add fastmcp package
cmd.extend(["--with", "fastmcp"])
# Add additional packages
if with_packages:
for pkg in with_packages:
if pkg:
cmd.extend(["--with", pkg])
# Add requirements file
if with_requirements:
cmd.extend(["--with-requirements", str(with_requirements)])
# Add fastmcp run command
cmd.extend(["fastmcp", "run", server_spec])
# Build uv command using Environment.build_uv_args()
env_config = Environment(
python=python_version,
dependencies=with_packages if with_packages else None,
requirements=str(with_requirements.resolve()) if with_requirements else None,
project=str(project.resolve()) if project else None,
editable=editable,
)
# IMPORTANT: We add --skip-env to prevent infinite recursion.
# When this function executes `uv run ... fastmcp run server.py`, the inner
# `fastmcp run` command will be executed inside the uv environment we're creating.
# Without --skip-env, that inner command would detect it needs uv (due to the same
# CLI args) and try to spawn ANOTHER uv subprocess, creating infinite recursion.
# The --skip-env flag tells the inner fastmcp: "skip environment setup, we're already
# inside the uv environment that was just created for us."
cmd = ["uv"] + env_config.build_uv_args(
["fastmcp", "run", server_spec, "--skip-env"]
)
# Add transport options
if transport:
@ -336,16 +194,14 @@ def create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]:
return server
def load_fastmcp_config(
config_path: Path,
) -> tuple[EntrypointConfig, DeploymentConfig | None, EnvironmentConfig | None]:
def load_fastmcp_config(config_path: Path) -> FastMCPConfig:
"""Load a FastMCP configuration from a fastmcp.json file.
Args:
config_path: Path to fastmcp.json file
Returns:
Tuple of (entrypoint, deployment config, environment config)
FastMCPConfig object
"""
config = FastMCPConfig.from_file(config_path)
@ -353,55 +209,7 @@ def load_fastmcp_config(
if config.deployment:
config.deployment.apply_runtime_settings(config_path)
# Get entrypoint as structured object with resolved paths
entrypoint = config.get_entrypoint(config_path)
# Return None for empty configs (backward compatibility)
deployment = (
config.deployment
if any(
getattr(config.deployment, field, None) is not None
for field in DeploymentConfig.model_fields
)
else None
)
environment = (
config.environment
if any(
getattr(config.environment, field, None) is not None
for field in EnvironmentConfig.model_fields
)
else None
)
return entrypoint, deployment, environment
async def import_server_with_args(
file: Path,
server_or_factory: str | None = None,
server_args: list[str] | None = None,
) -> Any:
"""Import a server with optional command line arguments.
Args:
file: Path to the server file
server_or_factory: Optional server object or factory function name
server_args: Optional command line arguments to inject
Returns:
The imported server object
"""
if server_args:
original_argv = sys.argv[:]
try:
sys.argv = [str(file)] + server_args
return await import_server(file, server_or_factory)
finally:
sys.argv = original_argv
else:
return await import_server(file, server_or_factory)
return config
async def run_command(
@ -414,6 +222,7 @@ async def run_command(
server_args: list[str] | None = None,
show_banner: bool = True,
use_direct_import: bool = False,
skip_source: bool = False,
) -> None:
"""Run a MCP server or connect to a remote one.
@ -427,49 +236,76 @@ async def run_command(
server_args: Additional arguments to pass to the server
show_banner: Whether to show the server banner
use_direct_import: Whether to use direct import instead of subprocess
skip_source: Whether to skip source preparation step
"""
# Special case: URLs
if is_url(server_spec):
# Handle URL case
server = create_client_server(server_spec)
logger.debug(f"Created client proxy server for {server_spec}")
elif (
server_spec.endswith("fastmcp.json") or "fastmcp.json" in Path(server_spec).name
):
# Handle fastmcp.json configuration file (matches test_fastmcp.json, my.fastmcp.json, etc)
config_path = Path(server_spec)
entrypoint, deployment, environment = load_fastmcp_config(config_path)
# Merge deployment config with CLI arguments (CLI takes precedence)
if deployment:
merged = deployment.merge_with_cli_args(
transport=transport,
host=host,
port=port,
path=path,
log_level=log_level,
server_args=server_args,
)
transport = merged["transport"]
host = merged["host"]
port = merged["port"]
path = merged["path"]
log_level = merged["log_level"]
server_args = merged["server_args"]
# Import the server from the structured entrypoint
file_path = Path(entrypoint.file)
server = await import_server_with_args(
file_path, entrypoint.object, server_args
)
logger.debug(f'Found server "{server.name}" from config {config_path}')
# Special case: MCPConfig files (legacy)
elif server_spec.endswith(".json"):
# Handle other JSON files as MCPConfig
server = create_mcp_config_server(Path(server_spec))
# Load JSON and check which type of config it is
config_path = Path(server_spec)
with open(config_path) as f:
data = json.load(f)
# Check if it's an MCPConfig first (has canonical mcpServers key)
if "mcpServers" in data:
# It's an MCP config
server = create_mcp_config_server(config_path)
else:
# It's a FastMCP config - load it properly
config = load_fastmcp_config(config_path)
# Merge deployment config with CLI arguments (CLI takes precedence)
if config.deployment:
transport = transport or config.deployment.transport
host = host or config.deployment.host
port = port or config.deployment.port
path = path or config.deployment.path
log_level = log_level or config.deployment.log_level
server_args = (
server_args if server_args is not None else config.deployment.args
)
# Prepare the source if needed (e.g., clone git repo, download from cloud)
if not skip_source:
await config.source.prepare()
# Load the server using the source
from contextlib import nullcontext
from fastmcp.cli.cli import with_argv
# Use sys.argv context manager if deployment args specified
argv_context = with_argv(server_args) if server_args else nullcontext()
with argv_context:
server = await config.source.load_server()
logger.debug(f'Found server "{server.name}" from config {config_path}')
else:
# Handle file case
file, server_or_factory = parse_file_path(server_spec)
server = await import_server_with_args(file, server_or_factory, server_args)
logger.debug(f'Found server "{server.name}" in {file}')
# Regular file case - create a FastMCPConfig with FileSystemSource
source = FileSystemSource(path=server_spec)
config = FastMCPConfig(source=source)
# Prepare the source if needed
if not skip_source:
await config.source.prepare()
# Load the server
from contextlib import nullcontext
from fastmcp.cli.cli import with_argv
# Use sys.argv context manager if server_args specified
argv_context = with_argv(server_args) if server_args else nullcontext()
with argv_context:
server = await config.source.load_server()
logger.debug(f'Found server "{server.name}" in {source.path}')
# Run the server
@ -506,6 +342,8 @@ def run_v1_server(
port: int | None = None,
transport: TransportType | None = None,
) -> None:
from functools import partial
if host:
server.settings.host = host
if port:

View file

@ -217,8 +217,13 @@ class OAuth(OAuthClientProvider):
self.redirect_port = callback_port or find_available_port()
redirect_uri = f"http://localhost:{self.redirect_port}/callback"
scopes_str: str
if isinstance(scopes, list):
scopes = " ".join(scopes)
scopes_str = " ".join(scopes)
elif scopes is not None:
scopes_str = str(scopes)
else:
scopes_str = ""
client_metadata = OAuthClientMetadata(
client_name=client_name,
@ -226,7 +231,7 @@ class OAuth(OAuthClientProvider):
grant_types=["authorization_code", "refresh_token"],
response_types=["code"],
# token_endpoint_auth_method="client_secret_post",
scope=scopes,
scope=scopes_str,
**(additional_client_metadata or {}),
)

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
import copy
import datetime
import secrets
from contextlib import AsyncExitStack, asynccontextmanager
from dataclasses import dataclass, field
from pathlib import Path
@ -212,6 +213,7 @@ class Client(Generic[ClientTransportT]):
| dict[str, Any]
| str
),
name: str | None = None,
roots: RootsList | RootsHandler | None = None,
sampling_handler: ClientSamplingHandler | None = None,
elicitation_handler: ElicitationHandler | None = None,
@ -223,6 +225,8 @@ class Client(Generic[ClientTransportT]):
client_info: mcp.types.Implementation | None = None,
auth: httpx.Auth | Literal["oauth"] | str | None = None,
) -> None:
self.name = name or self.generate_name()
self.transport = cast(ClientTransportT, infer_transport(transport))
if auth is not None:
self.transport._set_auth(auth)
@ -236,7 +240,7 @@ class Client(Generic[ClientTransportT]):
self._progress_handler = progress_handler
if isinstance(timeout, int | float):
timeout = datetime.timedelta(seconds=timeout)
timeout = datetime.timedelta(seconds=float(timeout))
# handle init handshake timeout
if init_timeout is None:
@ -339,6 +343,8 @@ class Client(Generic[ClientTransportT]):
# Reset session state to fresh state
new_client._session_state = ClientSessionState()
new_client.name += f":{secrets.token_hex(2)}"
return new_client
@asynccontextmanager
@ -538,6 +544,8 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
logger.debug(f"[{self.name}] called list_resources")
result = await self.session.list_resources()
return result
@ -565,6 +573,8 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
logger.debug(f"[{self.name}] called list_resource_templates")
result = await self.session.list_resource_templates()
return result
@ -597,6 +607,8 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
logger.debug(f"[{self.name}] called read_resource: {uri}")
if isinstance(uri, str):
uri = AnyUrl(uri) # Ensure AnyUrl
result = await self.session.read_resource(uri)
@ -651,6 +663,8 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
logger.debug(f"[{self.name}] called list_prompts")
result = await self.session.list_prompts()
return result
@ -683,6 +697,8 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
logger.debug(f"[{self.name}] called get_prompt: {name}")
# Serialize arguments for MCP protocol - convert non-string values to JSON
serialized_arguments: dict[str, str] | None = None
if arguments:
@ -740,6 +756,8 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
logger.debug(f"[{self.name}] called complete: {ref}")
result = await self.session.complete(ref=ref, argument=argument)
return result
@ -775,6 +793,8 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
logger.debug(f"[{self.name}] called list_tools")
result = await self.session.list_tools()
return result
@ -817,9 +837,10 @@ class Client(Generic[ClientTransportT]):
Raises:
RuntimeError: If called while the client is not connected.
"""
logger.debug(f"[{self.name}] called call_tool: {name}")
if isinstance(timeout, int | float):
timeout = datetime.timedelta(seconds=timeout)
timeout = datetime.timedelta(seconds=float(timeout))
result = await self.session.call_tool(
name=name,
arguments=arguments,
@ -889,7 +910,7 @@ class Client(Generic[ClientTransportT]):
else:
data = result.structuredContent
except Exception as e:
logger.error(f"Error parsing structured content: {e}")
logger.error(f"[{self.name}] Error parsing structured content: {e}")
return CallToolResult(
content=result.content,
@ -898,6 +919,14 @@ class Client(Generic[ClientTransportT]):
is_error=result.isError,
)
@classmethod
def generate_name(cls, name: str | None = None) -> str:
class_name = cls.__name__
if name is None:
return f"{class_name}-{secrets.token_hex(2)}"
else:
return f"{class_name}-{name}-{secrets.token_hex(2)}"
@dataclass
class CallToolResult:

View file

@ -36,7 +36,7 @@ from fastmcp.client.auth.oauth import OAuth
from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url
from fastmcp.server.dependencies import get_http_headers
from fastmcp.server.server import FastMCP
from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import EnvironmentConfig
from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import Environment
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
@ -182,7 +182,7 @@ class SSETransport(ClientTransport):
self.httpx_client_factory = httpx_client_factory
if isinstance(sse_read_timeout, int | float):
sse_read_timeout = datetime.timedelta(seconds=sse_read_timeout)
sse_read_timeout = datetime.timedelta(seconds=float(sse_read_timeout))
self.sse_read_timeout = sse_read_timeout
def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
@ -252,7 +252,7 @@ class StreamableHttpTransport(ClientTransport):
self.httpx_client_factory = httpx_client_factory
if isinstance(sse_read_timeout, int | float):
sse_read_timeout = datetime.timedelta(seconds=sse_read_timeout)
sse_read_timeout = datetime.timedelta(seconds=float(sse_read_timeout))
self.sse_read_timeout = sse_read_timeout
def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
@ -596,8 +596,8 @@ class UvStdioTransport(StdioTransport):
f"Project directory not found: {project_directory}"
)
# Create EnvironmentConfig from provided parameters (internal use)
env_config = EnvironmentConfig(
# Create Environment from provided parameters (internal use)
env_config = Environment(
python=python_version,
dependencies=with_packages,
requirements=with_requirements,
@ -902,7 +902,8 @@ class MCPConfigTransport(ClientTransport):
# otherwise create a composite client
else:
self._composite_server = FastMCP[Any]()
name = FastMCP.generate_name("MCPRouter")
self._composite_server = FastMCP[Any](name=name)
for name, server, transport in mcp_config_to_servers_and_transports(
self.config
@ -1024,28 +1025,36 @@ def infer_transport(
# the transport is a FastMCP server (2.x or 1.0)
elif isinstance(transport, FastMCP | FastMCP1Server):
inferred_transport = FastMCPTransport(mcp=transport)
inferred_transport = FastMCPTransport(
mcp=cast(FastMCP[Any] | FastMCP1Server, transport)
)
# the transport is a path to a script
elif isinstance(transport, Path | str) and Path(transport).exists():
if str(transport).endswith(".py"):
inferred_transport = PythonStdioTransport(script_path=transport)
inferred_transport = PythonStdioTransport(script_path=cast(Path, transport))
elif str(transport).endswith(".js"):
inferred_transport = NodeStdioTransport(script_path=transport)
inferred_transport = NodeStdioTransport(script_path=cast(Path, transport))
else:
raise ValueError(f"Unsupported script type: {transport}")
# the transport is an http(s) URL
elif isinstance(transport, AnyUrl | str) and str(transport).startswith("http"):
inferred_transport_type = infer_transport_type_from_url(transport)
inferred_transport_type = infer_transport_type_from_url(
cast(AnyUrl | str, transport)
)
if inferred_transport_type == "sse":
inferred_transport = SSETransport(url=transport)
inferred_transport = SSETransport(url=cast(AnyUrl | str, transport))
else:
inferred_transport = StreamableHttpTransport(url=transport)
inferred_transport = StreamableHttpTransport(
url=cast(AnyUrl | str, transport)
)
# if the transport is a config dict or MCPConfig
elif isinstance(transport, dict | MCPConfig):
inferred_transport = MCPConfigTransport(config=transport)
inferred_transport = MCPConfigTransport(
config=cast(dict | MCPConfig, transport)
)
# the transport is an unknown type
else:

View file

@ -27,7 +27,7 @@ from __future__ import annotations
import datetime
import re
from pathlib import Path
from typing import TYPE_CHECKING, Annotated, Any, Literal
from typing import TYPE_CHECKING, Annotated, Any, Literal, cast
from urllib.parse import urlparse
import httpx
@ -91,14 +91,24 @@ class _TransformingMCPServerMixin(FastMCPBaseModel):
def _to_server_and_underlying_transport(
self,
server_name: str | None = None,
client_name: str | None = None,
) -> tuple[FastMCP[Any], ClientTransport]:
"""Turn the Transforming MCPServer into a FastMCP Server and also return the underlying transport."""
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.client.transports import (
ClientTransport, # pyright: ignore[reportUnusedImport]
)
transport: ClientTransport = super().to_transport() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType]
transport = cast(ClientTransport, transport)
client: Client[ClientTransport] = Client(transport=transport, name=client_name)
wrapped_mcp_server = FastMCP.as_proxy(
transport,
name=server_name,
backend=client,
tool_transformations=self.tools,
include_tags=self.include_tags,
exclude_tags=self.exclude_tags,

View file

@ -43,6 +43,7 @@ from starlette.responses import JSONResponse, RedirectResponse
from starlette.routing import Route
from fastmcp.server.auth.auth import OAuthProvider, TokenVerifier
from fastmcp.server.auth.redirect_validation import validate_redirect_uri
from fastmcp.utilities.logging import get_logger
if TYPE_CHECKING:
@ -52,7 +53,7 @@ logger = get_logger(__name__)
class ProxyDCRClient(OAuthClientInformationFull):
"""Client for DCR proxy that accepts any localhost redirect URI.
"""Client for DCR proxy with configurable redirect URI validation.
This special client class is critical for the OAuth proxy to work correctly
with Dynamic Client Registration (DCR). Here's why it exists:
@ -61,36 +62,48 @@ class ProxyDCRClient(OAuthClientInformationFull):
--------
When MCP clients use OAuth, they dynamically register with random localhost
ports (e.g., http://localhost:55454/callback). The OAuth proxy needs to:
1. Accept these dynamic redirect URIs from clients
1. Accept these dynamic redirect URIs from clients based on configured patterns
2. Use its own fixed redirect URI with the upstream provider (Google, GitHub, etc.)
3. Forward the authorization code back to the client's dynamic URI
Solution:
---------
This class overrides redirect_uri validation to accept ANY localhost URI,
This class validates redirect URIs against configurable patterns,
while the proxy internally uses its own fixed redirect URI with the upstream
provider. This allows the flow to work even when clients reconnect with
different ports or when tokens are cached.
Without this class, clients would get "Redirect URI not registered" errors
when trying to authenticate with cached tokens, because the stored client
would have fixed redirect URIs that don't match the new dynamic port.
Without proper validation, clients could get "Redirect URI not registered" errors
when trying to authenticate with cached tokens, or security vulnerabilities could
arise from accepting arbitrary redirect URIs.
"""
def __init__(
self, *args, allowed_redirect_uri_patterns: list[str] | None = None, **kwargs
):
"""Initialize with allowed redirect URI patterns.
Args:
allowed_redirect_uri_patterns: List of allowed redirect URI patterns with wildcard support.
If None, defaults to localhost-only patterns.
If empty list, allows all redirect URIs.
"""
super().__init__(*args, **kwargs)
self._allowed_redirect_uri_patterns = allowed_redirect_uri_patterns
def validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl:
"""Accept any localhost redirect URI for DCR clients.
"""Validate redirect URI against allowed patterns.
Since we're acting as a proxy and clients register dynamically,
we need to accept their localhost redirect URIs even though they're
not pre-registered with us. This is essential for cached token
scenarios where the client may reconnect with a different port.
we validate their redirect URIs against configurable patterns.
This is essential for cached token scenarios where the client may
reconnect with a different port.
"""
if redirect_uri is not None:
# Accept any localhost redirect URI for DCR clients
uri_str = str(redirect_uri)
if uri_str.startswith(("http://localhost", "http://127.0.0.1")):
# Validate against allowed patterns
if validate_redirect_uri(redirect_uri, self._allowed_redirect_uri_patterns):
return redirect_uri
# Fall back to normal validation for non-localhost URIs
# Fall back to normal validation if not in allowed patterns
return super().validate_redirect_uri(redirect_uri)
# If no redirect_uri provided, use default behavior
return super().validate_redirect_uri(redirect_uri)
@ -229,6 +242,8 @@ class OAuthProxy(OAuthProvider):
issuer_url: AnyHttpUrl | str | None = None,
service_documentation_url: AnyHttpUrl | str | None = None,
resource_server_url: AnyHttpUrl | str | None = None,
# Client redirect URI validation
allowed_client_redirect_uris: list[str] | None = None,
):
"""Initialize the OAuth proxy provider.
@ -244,6 +259,11 @@ class OAuthProxy(OAuthProvider):
issuer_url: Issuer URL for OAuth metadata (defaults to base_url)
service_documentation_url: Optional service documentation URL
resource_server_url: Resource server URL (defaults to base_url)
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
Patterns support wildcards (e.g., "http://localhost:*", "https://*.example.com/*").
If None (default), only localhost redirect URIs are allowed.
If empty list, all redirect URIs are allowed (not recommended for production).
These are for MCP clients performing loopback redirects, NOT for the upstream OAuth app.
"""
# Convert string URLs to AnyHttpUrl for parent class
base_url_parsed = (
@ -302,6 +322,7 @@ class OAuthProxy(OAuthProvider):
self._redirect_path = (
redirect_path if redirect_path.startswith("/") else f"/{redirect_path}"
)
self._allowed_client_redirect_uris = allowed_client_redirect_uris
# Local state for DCR and token bookkeeping
self._clients: dict[str, OAuthClientInformationFull] = {}
@ -353,9 +374,10 @@ class OAuthProxy(OAuthProvider):
client_secret=None,
redirect_uris=[
AnyUrl("http://localhost")
], # Placeholder - we accept any localhost URI
], # Placeholder, validation uses allowed_patterns
grant_types=["authorization_code", "refresh_token"],
token_endpoint_auth_method="none",
allowed_redirect_uri_patterns=self._allowed_client_redirect_uris,
)
logger.debug("Created ProxyDCRClient for unregistered client %s", client_id)
@ -386,7 +408,7 @@ class OAuthProxy(OAuthProvider):
upstream_id = self._upstream_client_id
upstream_secret = self._upstream_client_secret.get_secret_value()
# Create a ProxyDCRClient that accepts any localhost redirect URI
# Create a ProxyDCRClient with configured redirect URI validation
proxy_client = ProxyDCRClient(
client_id=upstream_id,
client_secret=upstream_secret,
@ -394,17 +416,9 @@ class OAuthProxy(OAuthProvider):
grant_types=client_info.grant_types
or ["authorization_code", "refresh_token"],
token_endpoint_auth_method="none",
allowed_redirect_uri_patterns=self._allowed_client_redirect_uris,
)
# Modify the client_info object in place (framework ignores return values)
client_info.client_id = upstream_id
client_info.client_secret = upstream_secret
client_info.token_endpoint_auth_method = "none"
# Ensure correct grant types
if not client_info.grant_types:
client_info.grant_types = ["authorization_code", "refresh_token"]
# Store the ProxyDCRClient using the upstream ID
self._clients[upstream_id] = proxy_client

View file

@ -159,17 +159,20 @@ class JWTVerifierSettings(BaseSettings):
@register_provider("JWT")
class JWTVerifier(TokenVerifier):
"""
JWT token verifier using public key or JWKS.
JWT token verifier supporting both asymmetric (RSA/ECDSA) and symmetric (HMAC) algorithms.
This verifier validates JWT tokens signed by an external issuer. It's ideal for
scenarios where you have a centralized identity provider (like Auth0, Okta, or
your own OAuth server) that issues JWTs, and your FastMCP server acts as a
resource server validating those tokens.
This verifier validates JWT tokens using various signing algorithms:
- **Asymmetric algorithms** (RS256/384/512, ES256/384/512, PS256/384/512):
Uses public/private key pairs. Ideal for external clients and services where
only the authorization server has the private key.
- **Symmetric algorithms** (HS256/384/512): Uses a shared secret for both
signing and verification. Perfect for internal microservices and trusted
environments where the secret can be securely shared.
Use this when:
- You have JWT tokens issued by an external service
- You want asymmetric key verification (public/private key pairs)
- You need JWKS support for automatic key rotation
- You have JWT tokens issued by an external service (asymmetric)
- You need JWKS support for automatic key rotation (asymmetric)
- You have internal microservices sharing a secret key (symmetric)
- Your tokens contain standard OAuth scopes and claims
"""
@ -188,11 +191,14 @@ class JWTVerifier(TokenVerifier):
Initialize the JWT token verifier.
Args:
public_key: PEM-encoded public key for verification
jwks_uri: URI to fetch JSON Web Key Set
public_key: For asymmetric algorithms (RS256, ES256, etc.): PEM-encoded public key.
For symmetric algorithms (HS256, HS384, HS512): The shared secret string.
jwks_uri: URI to fetch JSON Web Key Set (only for asymmetric algorithms)
issuer: Expected issuer claim
audience: Expected audience claim(s)
algorithm: JWT signing algorithm (default: RS256)
algorithm: JWT signing algorithm. Supported algorithms:
- Asymmetric: RS256/384/512, ES256/384/512, PS256/384/512 (default: RS256)
- Symmetric: HS256, HS384, HS512
required_scopes: Required scopes for all tokens
resource_server_url: Resource server URL for TokenVerifier protocol
"""

View file

@ -0,0 +1,70 @@
"""Utilities for validating client redirect URIs in OAuth flows."""
import fnmatch
from pydantic import AnyUrl
def matches_allowed_pattern(uri: str, pattern: str) -> bool:
"""Check if a URI matches an allowed pattern with wildcard support.
Patterns support * wildcard matching:
- http://localhost:* matches any localhost port
- http://127.0.0.1:* matches any 127.0.0.1 port
- https://*.example.com/* matches any subdomain of example.com
- https://app.example.com/auth/* matches any path under /auth/
Args:
uri: The redirect URI to validate
pattern: The allowed pattern (may contain wildcards)
Returns:
True if the URI matches the pattern
"""
# Use fnmatch for wildcard matching
return fnmatch.fnmatch(uri, pattern)
def validate_redirect_uri(
redirect_uri: str | AnyUrl | None,
allowed_patterns: list[str] | None,
) -> bool:
"""Validate a redirect URI against allowed patterns.
Args:
redirect_uri: The redirect URI to validate
allowed_patterns: List of allowed patterns. If None, defaults to localhost.
If empty list, all URIs are allowed.
Returns:
True if the redirect URI is allowed
"""
if redirect_uri is None:
return True # None is allowed (will use client's default)
uri_str = str(redirect_uri)
# If no patterns specified, default to localhost only
if allowed_patterns is None:
allowed_patterns = [
"http://localhost:*",
"http://127.0.0.1:*",
]
# Empty list means allow all
if len(allowed_patterns) == 0:
return True
# Check if URI matches any allowed pattern
for pattern in allowed_patterns:
if matches_allowed_pattern(uri_str, pattern):
return True
return False
# Default patterns for localhost-only validation
DEFAULT_LOCALHOST_PATTERNS = [
"http://localhost:*",
"http://127.0.0.1:*",
]

View file

@ -548,7 +548,7 @@ class Context:
if isinstance(validated_data, ScalarElicitationType):
return AcceptedElicitation[T](data=validated_data.value)
else:
return AcceptedElicitation[T](data=validated_data)
return AcceptedElicitation[T](data=cast(T, validated_data))
elif result.content:
raise ValueError(
"Elicitation expected an empty response, but received: "

View file

@ -546,6 +546,8 @@ class ProxyClient(Client[ClientTransportT]):
| str,
**kwargs,
):
if "name" not in kwargs:
kwargs["name"] = self.generate_name()
if "roots" not in kwargs:
kwargs["roots"] = default_proxy_roots_handler
if "sampling_handler" not in kwargs:

View file

@ -5,6 +5,7 @@ from __future__ import annotations
import inspect
import json
import re
import secrets
import warnings
from collections.abc import AsyncIterator, Awaitable, Callable
from contextlib import (
@ -197,8 +198,9 @@ class FastMCP(Generic[LifespanResultT]):
lifespan = default_lifespan
else:
self._has_lifespan = True
# Generate random ID if no name provided
self._mcp_server = LowLevelServer[LifespanResultT](
name=name or "FastMCP",
name=name or self.generate_name(),
version=version,
instructions=instructions,
lifespan=_lifespan_wrapper(self, lifespan),
@ -519,7 +521,7 @@ class FastMCP(Generic[LifespanResultT]):
return routes
async def _mcp_list_tools(self) -> list[MCPTool]:
logger.debug("Handler called: list_tools")
logger.debug(f"[{self.name}] Handler called: list_tools")
async with fastmcp.server.context.Context(fastmcp=self):
tools = await self._list_tools()
@ -563,7 +565,7 @@ class FastMCP(Generic[LifespanResultT]):
return await self._apply_middleware(mw_context, _handler)
async def _mcp_list_resources(self) -> list[MCPResource]:
logger.debug("Handler called: list_resources")
logger.debug(f"[{self.name}] Handler called: list_resources")
async with fastmcp.server.context.Context(fastmcp=self):
resources = await self._list_resources()
@ -608,7 +610,7 @@ class FastMCP(Generic[LifespanResultT]):
return await self._apply_middleware(mw_context, _handler)
async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
logger.debug("Handler called: list_resource_templates")
logger.debug(f"[{self.name}] Handler called: list_resource_templates")
async with fastmcp.server.context.Context(fastmcp=self):
templates = await self._list_resource_templates()
@ -653,7 +655,7 @@ class FastMCP(Generic[LifespanResultT]):
return await self._apply_middleware(mw_context, _handler)
async def _mcp_list_prompts(self) -> list[MCPPrompt]:
logger.debug("Handler called: list_prompts")
logger.debug(f"[{self.name}] Handler called: list_prompts")
async with fastmcp.server.context.Context(fastmcp=self):
prompts = await self._list_prompts()
@ -712,7 +714,9 @@ class FastMCP(Generic[LifespanResultT]):
Returns:
List of MCP Content objects containing the tool results
"""
logger.debug("Handler called: call_tool %s with %s", key, arguments)
logger.debug(
f"[{self.name}] Handler called: call_tool %s with %s", key, arguments
)
async with fastmcp.server.context.Context(fastmcp=self):
try:
@ -754,7 +758,7 @@ class FastMCP(Generic[LifespanResultT]):
Delegates to _read_resource, which should be overridden by FastMCP subclasses.
"""
logger.debug("Handler called: read_resource %s", uri)
logger.debug(f"[{self.name}] Handler called: read_resource %s", uri)
async with fastmcp.server.context.Context(fastmcp=self):
try:
@ -809,7 +813,9 @@ class FastMCP(Generic[LifespanResultT]):
Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
"""
logger.debug("Handler called: get_prompt %s with %s", name, arguments)
logger.debug(
f"[{self.name}] Handler called: get_prompt %s with %s", name, arguments
)
async with fastmcp.server.context.Context(fastmcp=self):
try:
@ -1027,7 +1033,7 @@ class FastMCP(Generic[LifespanResultT]):
description=description,
tags=tags,
output_schema=output_schema,
annotations=annotations,
annotations=cast(ToolAnnotations | None, annotations),
exclude_args=exclude_args,
meta=meta,
serializer=self._tool_serializer,
@ -1257,7 +1263,7 @@ class FastMCP(Generic[LifespanResultT]):
mime_type=mime_type,
tags=tags,
enabled=enabled,
annotations=annotations,
annotations=cast(Annotations | None, annotations),
meta=meta,
)
self.add_template(template)
@ -1272,7 +1278,7 @@ class FastMCP(Generic[LifespanResultT]):
mime_type=mime_type,
tags=tags,
enabled=enabled,
annotations=annotations,
annotations=cast(Annotations | None, annotations),
meta=meta,
)
self.add_resource(resource)
@ -1966,9 +1972,11 @@ class FastMCP(Generic[LifespanResultT]):
self._prompt_manager.add_prompt(prompt)
if prefix:
logger.debug(f"Imported server {server.name} with prefix '{prefix}'")
logger.debug(
f"[{self.name}] Imported server {server.name} with prefix '{prefix}'"
)
else:
logger.debug(f"Imported server {server.name}")
logger.debug(f"[{self.name}] Imported server {server.name}")
@classmethod
def from_openapi(
@ -2195,6 +2203,15 @@ class FastMCP(Generic[LifespanResultT]):
return True
@classmethod
def generate_name(cls, name: str | None = None) -> str:
class_name = cls.__name__
if name is None:
return f"{class_name}-{secrets.token_hex(2)}"
else:
return f"{class_name}-{name}-{secrets.token_hex(2)}"
@dataclass
class MountedServer:

View file

@ -146,6 +146,7 @@ class Settings(BaseSettings):
test_mode: bool = False
log_enabled: bool = True
log_level: LOG_LEVEL = "INFO"
@field_validator("log_level", mode="before")
@ -314,7 +315,7 @@ class Settings(BaseSettings):
Whether to include FastMCP meta in the server's MCP responses.
If True, a `_fastmcp` key will be added to the `meta` field of
all MCP component responses. This key will contain a dict of
various FastMCP-specific metadata, such as tags.
various FastMCP-specific metadata, such as tags.
"""
),
),

View file

@ -5,17 +5,19 @@ The current version is v1, which is re-exported here for convenience.
"""
from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import (
DeploymentConfig,
EntrypointConfig,
EnvironmentConfig,
Deployment,
Environment,
FastMCPConfig,
generate_schema,
)
from fastmcp.utilities.fastmcp_config.v1.sources.base import BaseSource
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
__all__ = [
"BaseSource",
"Deployment",
"Environment",
"FastMCPConfig",
"EntrypointConfig",
"EnvironmentConfig",
"DeploymentConfig",
"FileSystemSource",
"generate_schema",
]

View file

@ -15,36 +15,20 @@ from typing import TYPE_CHECKING, Any, Literal, overload
from pydantic import BaseModel, Field, field_validator
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
from fastmcp.utilities.logging import get_logger
logger = get_logger("cli.config")
# JSON Schema for IDE support
FASTMCP_JSON_SCHEMA = "https://gofastmcp.com/schemas/fastmcp_config/v1.json"
FASTMCP_JSON_SCHEMA = "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json"
class EntrypointConfig(BaseModel):
"""Configuration for server entrypoint when using object format."""
file: str = Field(
description="Path to Python file containing the server",
examples=["server.py", "src/server.py", "app/main.py"],
)
object: str | None = Field(
default=None,
description="Name of the server object in the file (defaults to searching for mcp/server/app)",
examples=["app", "mcp", "server"],
)
repo: str | None = Field(
default=None,
description="Git repository URL",
examples=["https://github.com/user/repo"],
)
# Type alias for source union (will expand with GitSource, etc in future)
SourceType = FileSystemSource
class EnvironmentConfig(BaseModel):
class Environment(BaseModel):
"""Configuration for Python environment setup."""
python: str | None = Field(
@ -96,13 +80,18 @@ class EnvironmentConfig(BaseModel):
if self.project:
args.extend(["--project", str(self.project)])
# Add fastmcp as a base dependency
args.extend(["--with", "fastmcp"])
# Add fastmcp dependency - use editable install if in development mode
dev_path = self._find_fastmcp_dev_path()
if dev_path:
args.extend(["--with-editable", str(dev_path)])
else:
args.extend(["--with", "fastmcp"])
# Add additional dependencies
# Add additional dependencies (skip fastmcp if already added)
if self.dependencies:
for dep in self.dependencies:
args.extend(["--with", dep])
if dep != "fastmcp": # Skip fastmcp since we already added it
args.extend(["--with", dep])
# Add requirements file
if self.requirements:
@ -121,6 +110,34 @@ class EnvironmentConfig(BaseModel):
return args
def _find_fastmcp_dev_path(self) -> Path | None:
"""Find the fastmcp development directory by looking for pyproject.toml.
Searches from the current working directory up the directory tree
looking for a pyproject.toml file that contains name = "fastmcp".
Returns:
Path to the fastmcp project directory if found, None otherwise
"""
current_path = Path.cwd()
# Search up the directory tree
for path in [current_path] + list(current_path.parents):
pyproject_path = path / "pyproject.toml"
if pyproject_path.exists():
try:
# Read and check if this is the fastmcp project
content = pyproject_path.read_text(encoding="utf-8")
if 'name = "fastmcp"' in content or "name='fastmcp'" in content:
logger.debug(f"Found fastmcp development project at: {path}")
return path
except (OSError, UnicodeDecodeError):
# Skip files that can't be read
continue
logger.debug("No fastmcp development project found, using PyPI package")
return None
def run_with_uv(self, command: list[str]) -> None:
"""Execute a command using uv run with this environment configuration.
@ -150,51 +167,18 @@ class EnvironmentConfig(BaseModel):
Returns:
True if any environment settings require uv run
"""
return bool(
self.python
or self.dependencies
or self.requirements
or self.project
or self.editable
return any(
[
self.python is not None,
self.dependencies is not None,
self.requirements is not None,
self.project is not None,
self.editable is not None,
]
)
def merge_with_cli_args(
self,
python: str | None = None,
with_packages: list[str] | None = None,
with_requirements: Path | None = None,
project: Path | None = None,
with_editable: Path | None = None,
) -> dict[str, Any]:
"""Merge environment config with CLI arguments, with CLI taking precedence.
For packages, combines both config and CLI packages.
For other fields, CLI takes precedence if provided.
Returns:
Dictionary with merged arguments suitable for CLI commands
"""
from pathlib import Path
# Merge packages from both sources
packages = []
if self.dependencies:
packages.extend(self.dependencies)
if with_packages:
packages.extend(with_packages)
return {
"python": python or self.python,
"with_packages": packages,
"with_requirements": with_requirements
or (Path(self.requirements) if self.requirements else None),
"project": project or (Path(self.project) if self.project else None),
"with_editable": with_editable
or (Path(self.editable) if self.editable else None),
}
class DeploymentConfig(BaseModel):
class Deployment(BaseModel):
"""Configuration for server deployment and runtime settings."""
transport: Literal["stdio", "http", "sse"] | None = Field(
@ -243,29 +227,6 @@ class DeploymentConfig(BaseModel):
examples=[["--config", "config.json", "--debug"]],
)
def merge_with_cli_args(
self,
transport: str | None = None,
host: str | None = None,
port: int | None = None,
path: str | None = None,
log_level: str | None = None,
server_args: list[str] | None = None,
) -> dict[str, Any]:
"""Merge deployment config with CLI arguments, with CLI taking precedence.
Returns:
Dictionary with merged arguments suitable for CLI commands
"""
return {
"transport": transport or self.transport,
"host": host or self.host,
"port": port or self.port,
"path": path or self.path,
"log_level": log_level or self.log_level,
"server_args": server_args if server_args is not None else self.args,
}
def apply_runtime_settings(self, config_path: Path | None = None) -> None:
"""Apply runtime settings like environment variables and working directory.
@ -324,149 +285,99 @@ class FastMCPConfig(BaseModel):
# Schema field for IDE support
schema_: str | None = Field(
default="https://gofastmcp.com/schemas/fastmcp_config/v1.json",
default="https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
alias="$schema",
description="JSON schema for IDE support and validation",
)
# Server entrypoint - supports both string and object format
entrypoint: EntrypointConfig = Field(
description="Server entrypoint as a string (file or file:object) or object with file/object/repo",
# Server source - defines where and how to load the server
source: SourceType = Field(
description="Source configuration for the server",
examples=[
"server.py",
"server.py:app",
{"file": "src/server.py", "object": "app"},
{"path": "server.py"},
{"path": "server.py", "entrypoint": "app"},
{"type": "filesystem", "path": "src/server.py", "entrypoint": "mcp"},
],
)
# Environment configuration
environment: EnvironmentConfig = Field(
default_factory=lambda: EnvironmentConfig(),
environment: Environment = Field(
default_factory=lambda: Environment(),
description="Python environment setup configuration",
)
# Deployment configuration
deployment: DeploymentConfig = Field(
default_factory=lambda: DeploymentConfig(),
deployment: Deployment = Field(
default_factory=lambda: Deployment(),
description="Server deployment and runtime settings",
)
# purely for static type checkers to avoid issues with providng str entrypoint
# purely for static type checkers to avoid issues with providing dict source
if TYPE_CHECKING:
@overload
def __init__(
self, *, entrypoint: str | dict | EntrypointConfig, **data
) -> None: ...
def __init__(self, *, source: dict | FileSystemSource, **data) -> None: ...
@overload
def __init__(
self, *, environment: dict | EnvironmentConfig, **data
) -> None: ...
def __init__(self, *, environment: dict | Environment, **data) -> None: ...
@overload
def __init__(self, *, deployment: dict | DeploymentConfig, **data) -> None: ...
def __init__(self, *, deployment: dict | Deployment, **data) -> None: ...
def __init__(self, **data) -> None: ...
@field_validator("entrypoint", mode="before")
@field_validator("source", mode="before")
@classmethod
def validate_entrypoint(cls, v: str | EntrypointConfig) -> EntrypointConfig:
"""Validate and convert entrypoint to proper format.
def validate_source(cls, v: dict | FileSystemSource) -> FileSystemSource:
"""Validate and convert source to proper format.
Supports:
- String format: "server.py" or "server.py:object"
- Object format: {"file": "server.py", "object": "app"}
- EntrypointConfig instance (passed through)
- Dict format: {"path": "server.py", "entrypoint": "app"}
- FileSystemSource instance (passed through)
The string format with :object syntax is automatically parsed into
the object format for consistency.
No string parsing happens here - that's only at CLI boundaries.
FastMCPConfig works only with properly typed objects.
"""
if isinstance(v, EntrypointConfig):
# Already an EntrypointConfig instance, return as-is
if isinstance(v, FileSystemSource):
# Already a FileSystemSource instance, return as-is
return v
elif isinstance(v, dict):
return EntrypointConfig(**v)
elif isinstance(v, str):
# Parse file.py:object syntax into object format if present
if ":" in v:
# Check if it's a Windows path (e.g., C:\...)
has_windows_drive = len(v) > 1 and v[1] == ":"
# Only split if colon is not part of Windows drive
if ":" in (v[2:] if has_windows_drive else v):
file, obj = v.rsplit(":", 1)
return EntrypointConfig(file=file, object=obj)
else:
return EntrypointConfig(file=v)
raise ValueError("entrypoint must be a string, EntrypointConfig instance")
# Dict can have type field or not (filesystem is default)
if "type" not in v:
v["type"] = "filesystem"
return FileSystemSource(**v)
else:
raise ValueError("source must be a dict or FileSystemSource instance")
@field_validator("environment", mode="before")
@classmethod
def validate_environment(cls, v: dict | EnvironmentConfig) -> EnvironmentConfig:
"""Validate and convert environment to EnvironmentConfig.
def validate_environment(cls, v: dict | Environment) -> Environment:
"""Validate and convert environment to Environment.
Accepts:
- EnvironmentConfig instance
- dict that can be converted to EnvironmentConfig
- Environment instance
- dict that can be converted to Environment
"""
if isinstance(v, EnvironmentConfig):
if isinstance(v, Environment):
return v
elif isinstance(v, dict):
return EnvironmentConfig(**v) # type: ignore[arg-type]
return Environment(**v) # type: ignore[arg-type]
else:
raise ValueError("environment must be a dict, EnvironmentConfig instance")
raise ValueError("environment must be a dict, Environment instance")
@field_validator("deployment", mode="before")
@classmethod
def validate_deployment(cls, v: dict | DeploymentConfig) -> DeploymentConfig:
"""Validate and convert deployment to DeploymentConfig.
def validate_deployment(cls, v: dict | Deployment) -> Deployment:
"""Validate and convert deployment to Deployment.
Accepts:
- DeploymentConfig instance
- dict that can be converted to DeploymentConfig
- Deployment instance
- dict that can be converted to Deployment
"""
if isinstance(v, DeploymentConfig):
if isinstance(v, Deployment):
return v
elif isinstance(v, dict):
return DeploymentConfig(**v) # type: ignore[arg-type]
return Deployment(**v) # type: ignore[arg-type]
else:
raise ValueError("deployment must be a dict, DeploymentConfig instance")
def get_entrypoint(self, config_path: Path | None = None) -> EntrypointConfig:
"""Get the entrypoint as a structured object with resolved paths.
Args:
config_path: Path to config file for resolving relative paths
Returns:
EntrypointConfig object with file, object, and repo fields.
If config_path is provided, relative file paths are resolved
relative to the config file location.
"""
if isinstance(self.entrypoint, str):
# Parse string format into structured object
if ":" in self.entrypoint:
file, obj = self.entrypoint.rsplit(":", 1)
entrypoint = EntrypointConfig(file=file, object=obj)
else:
entrypoint = EntrypointConfig(file=self.entrypoint)
else:
# Already an EntrypointConfig
entrypoint = self.entrypoint
# Resolve relative paths if config_path provided
if config_path:
file_path = Path(entrypoint.file)
if not file_path.is_absolute():
resolved_path = (config_path.parent / file_path).resolve()
# Create new EntrypointConfig with resolved path
entrypoint = EntrypointConfig(
file=str(resolved_path),
object=entrypoint.object,
repo=entrypoint.repo,
)
return entrypoint
raise ValueError("deployment must be a dict, Deployment instance")
@classmethod
def from_file(cls, file_path: Path) -> FastMCPConfig:
@ -494,7 +405,7 @@ class FastMCPConfig(BaseModel):
@classmethod
def from_cli_args(
cls,
entrypoint: str,
source: FileSystemSource,
transport: Literal["stdio", "http", "sse", "streamable-http"] | None = None,
host: str | None = None,
port: int | None = None,
@ -516,7 +427,7 @@ class FastMCPConfig(BaseModel):
goes through a config object.
Args:
entrypoint: Server entrypoint (file or file:object)
source: Server source (FileSystemSource instance)
transport: Transport protocol
host: Host for HTTP transport
port: Port for HTTP transport
@ -537,7 +448,7 @@ class FastMCPConfig(BaseModel):
# Build environment config if any env args provided
environment = None
if any([python, dependencies, requirements, project, editable]):
environment = EnvironmentConfig(
environment = Environment(
python=python,
dependencies=dependencies,
requirements=requirements,
@ -551,7 +462,7 @@ class FastMCPConfig(BaseModel):
# Convert streamable-http to http for backward compatibility
if transport == "streamable-http":
transport = "http" # type: ignore[assignment]
deployment = DeploymentConfig(
deployment = Deployment(
transport=transport, # type: ignore[arg-type]
host=host,
port=port,
@ -563,7 +474,7 @@ class FastMCPConfig(BaseModel):
)
return cls(
entrypoint=entrypoint,
source=source,
environment=environment,
deployment=deployment,
)
@ -588,48 +499,6 @@ class FastMCPConfig(BaseModel):
return None
async def load_server(self, config_path: Path | None = None) -> Any:
"""Load the server from the configuration.
This handles environment setup, working directory changes,
and imports the server module.
Args:
config_path: Path to the config file (for resolving relative paths)
Returns:
The imported server object
"""
import os
from pathlib import Path
# Set environment variables if specified
if self.deployment and self.deployment.env:
for key, value in self.deployment.env.items():
os.environ[key] = value
# Change working directory if specified
if self.deployment and self.deployment.cwd:
cwd_path = Path(self.deployment.cwd)
if not cwd_path.is_absolute():
# If config_path provided, resolve relative to it
if config_path:
cwd_path = (config_path.parent / cwd_path).resolve()
else:
cwd_path = cwd_path.resolve()
os.chdir(cwd_path)
# Get structured entrypoint with resolved paths
entrypoint = self.get_entrypoint(config_path)
# Import the server
from fastmcp.cli.run import import_server_with_args
file_path = Path(entrypoint.file)
server_args = self.deployment.args if self.deployment else None
return await import_server_with_args(file_path, entrypoint.object, server_args)
async def run_server(self, **kwargs: Any) -> None:
"""Load and run the server with this configuration.
@ -637,7 +506,12 @@ class FastMCPConfig(BaseModel):
**kwargs: Additional arguments to pass to server.run_async()
These override config settings
"""
server = await self.load_server()
# Apply deployment settings (env vars, cwd)
if self.deployment:
self.deployment.apply_runtime_settings()
# Load the server
server = await self.source.load_server()
# Build run arguments from config
run_args = {}
@ -659,14 +533,19 @@ class FastMCPConfig(BaseModel):
await server.run_async(**run_args)
def generate_schema() -> dict[str, Any]:
def generate_schema(output_path: Path | str | None = None) -> dict[str, Any] | None:
"""Generate JSON schema for fastmcp.json files.
This is used to create the schema file that IDEs can use for
validation and auto-completion.
Args:
output_path: Optional path to write the schema to. If provided,
writes the schema and returns None. If not provided,
returns the schema as a dictionary.
Returns:
JSON schema as a dictionary
JSON schema as a dictionary if output_path is None, otherwise None
"""
schema = FastMCPConfig.model_json_schema()
@ -675,4 +554,14 @@ def generate_schema() -> dict[str, Any]:
schema["title"] = "FastMCP Configuration"
schema["description"] = "Configuration file for FastMCP servers"
if output_path:
import json
output = Path(output_path)
output.parent.mkdir(parents=True, exist_ok=True)
with open(output, "w") as f:
json.dump(schema, f, indent=2)
f.write("\n") # Add trailing newline
return None
return schema

View file

@ -1,6 +1,6 @@
{
"$defs": {
"DeploymentConfig": {
"Deployment": {
"description": "Configuration for server deployment and runtime settings.",
"properties": {
"transport": {
@ -159,64 +159,10 @@
"title": "Args"
}
},
"title": "DeploymentConfig",
"title": "Deployment",
"type": "object"
},
"EntrypointConfig": {
"description": "Configuration for server entrypoint when using object format.",
"properties": {
"file": {
"description": "Path to Python file containing the server",
"examples": [
"server.py",
"src/server.py",
"app/main.py"
],
"title": "File",
"type": "string"
},
"object": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Name of the server object in the file (defaults to searching for mcp/server/app)",
"examples": [
"app",
"mcp",
"server"
],
"title": "Object"
},
"repo": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Git repository URL",
"examples": [
"https://github.com/user/repo"
],
"title": "Repo"
}
},
"required": [
"file"
],
"title": "EntrypointConfig",
"type": "object"
},
"EnvironmentConfig": {
"Environment": {
"description": "Configuration for Python environment setup.",
"properties": {
"python": {
@ -312,7 +258,42 @@
"title": "Editable"
}
},
"title": "EnvironmentConfig",
"title": "Environment",
"type": "object"
},
"FileSystemSource": {
"description": "Source for local Python files.",
"properties": {
"type": {
"const": "filesystem",
"default": "filesystem",
"description": "Source type",
"title": "Type",
"type": "string"
},
"path": {
"description": "Path to Python file containing the server",
"title": "Path",
"type": "string"
},
"entrypoint": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
"title": "Entrypoint"
}
},
"required": [
"path"
],
"title": "FileSystemSource",
"type": "object"
}
},
@ -327,35 +308,41 @@
"type": "null"
}
],
"default": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"default": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"description": "JSON schema for IDE support and validation",
"title": "$Schema"
},
"entrypoint": {
"$ref": "#/$defs/EntrypointConfig",
"description": "Server entrypoint as a string (file or file:object) or object with file/object/repo",
"source": {
"$ref": "#/$defs/FileSystemSource",
"description": "Source configuration for the server",
"examples": [
"server.py",
"server.py:app",
{
"file": "src/server.py",
"object": "app"
"path": "server.py"
},
{
"entrypoint": "app",
"path": "server.py"
},
{
"entrypoint": "mcp",
"path": "src/server.py",
"type": "filesystem"
}
]
},
"environment": {
"$ref": "#/$defs/EnvironmentConfig",
"$ref": "#/$defs/Environment",
"description": "Python environment setup configuration"
},
"deployment": {
"$ref": "#/$defs/DeploymentConfig",
"$ref": "#/$defs/Deployment",
"description": "Server deployment and runtime settings"
}
},
"required": [
"entrypoint"
"source"
],
"title": "FastMCP Configuration",
"type": "object",
"$id": "https://gofastmcp.com/schemas/fastmcp_config/v1.json"
}
"$id": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json"
}

View file

@ -0,0 +1,30 @@
from abc import ABC, abstractmethod
from typing import Any
from pydantic import BaseModel, Field
class BaseSource(BaseModel, ABC):
"""Abstract base class for all source types."""
type: str = Field(description="Source type identifier")
async def prepare(self) -> None:
"""Prepare the source (download, clone, install, etc).
For sources that need preparation (e.g., git clone, download),
this method performs that preparation. For sources that don't
need preparation (e.g., local files), this is a no-op.
"""
# Default implementation for sources that don't need preparation
pass
@abstractmethod
async def load_server(self) -> Any:
"""Load and return the FastMCP server instance.
Must be called after prepare() if the source requires preparation.
All information needed to load the server should be available
as attributes on the source instance.
"""
...

View file

@ -0,0 +1,215 @@
import importlib.util
import inspect
import sys
from pathlib import Path
from typing import Any, Literal
from pydantic import Field, field_validator
from fastmcp.utilities.fastmcp_config.v1.sources.base import BaseSource
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
class FileSystemSource(BaseSource):
"""Source for local Python files."""
type: Literal["filesystem"] = Field(default="filesystem", description="Source type")
path: str = Field(description="Path to Python file containing the server")
entrypoint: str | None = Field(
default=None,
description="Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
)
@field_validator("path", mode="before")
@classmethod
def parse_path_with_object(cls, v: str) -> str:
"""Parse path:object syntax and extract the object name.
This validator runs before the model is created, allowing us to
handle the "file.py:object" syntax at the model boundary.
"""
if isinstance(v, str) and ":" in v:
# Check if it's a Windows path (e.g., C:\...)
has_windows_drive = len(v) > 1 and v[1] == ":"
# Only split if colon is not part of Windows drive
if ":" in (v[2:] if has_windows_drive else v):
# This path has an object specification
# We'll handle it in __init__ by setting entrypoint
return v
return v
def __init__(self, **data: Any) -> None:
"""Initialize FileSystemSource, handling path:object syntax."""
# Check if path contains an object specification
if "path" in data and isinstance(data["path"], str) and ":" in data["path"]:
path_str = data["path"]
# Check if it's a Windows path (e.g., C:\...)
has_windows_drive = len(path_str) > 1 and path_str[1] == ":"
# Only split if colon is not part of Windows drive
if ":" in (path_str[2:] if has_windows_drive else path_str):
file_str, obj = path_str.rsplit(":", 1)
data["path"] = file_str
# Only set entrypoint if not already provided
if "entrypoint" not in data or data["entrypoint"] is None:
data["entrypoint"] = obj
super().__init__(**data)
async def load_server(self) -> Any:
"""Load server from filesystem."""
# Resolve the file path
file_path = Path(self.path).expanduser().resolve()
if not file_path.exists():
logger.error(f"File not found: {file_path}")
sys.exit(1)
if not file_path.is_file():
logger.error(f"Not a file: {file_path}")
sys.exit(1)
# Import the module
module = self._import_module(file_path)
# Find the server object
server = await self._find_server_object(module, file_path)
return server
def _import_module(self, file_path: Path) -> Any:
"""Import a Python module from a file path.
Args:
file_path: Path to the Python file
Returns:
The imported module
"""
# Add parent directory to Python path so imports can be resolved
file_dir = str(file_path.parent)
if file_dir not in sys.path:
sys.path.insert(0, file_dir)
# Import the module
spec = importlib.util.spec_from_file_location("server_module", file_path)
if not spec or not spec.loader:
logger.error("Could not load module", extra={"file": str(file_path)})
sys.exit(1)
module = importlib.util.module_from_spec(spec) # type: ignore[arg-type]
sys.modules["server_module"] = module # Register in sys.modules
spec.loader.exec_module(module) # type: ignore[union-attr]
return module
async def _find_server_object(self, module: Any, file_path: Path) -> Any:
"""Find the server object in the module.
Args:
module: The imported Python module
file_path: Path to the file (for error messages)
Returns:
The server object (or result of calling a factory function)
"""
# Avoid circular import by importing here
from mcp.server.fastmcp import FastMCP as FastMCP1x
from fastmcp.server.server import FastMCP
# If entrypoint is specified, use it
if self.entrypoint:
# Handle module:object syntax (though this is legacy)
if ":" in self.entrypoint:
module_name, object_name = self.entrypoint.split(":", 1)
try:
import importlib
server_module = importlib.import_module(module_name)
obj = getattr(server_module, object_name, None)
except ImportError:
logger.error(
f"Could not import module '{module_name}'",
extra={"file": str(file_path)},
)
sys.exit(1)
else:
# Just object name
obj = getattr(module, self.entrypoint, None)
if obj is None:
logger.error(
f"Server object '{self.entrypoint}' not found",
extra={"file": str(file_path)},
)
sys.exit(1)
return await self._resolve_factory(obj, file_path, self.entrypoint)
# No entrypoint specified, try common server names
for name in ["mcp", "server", "app"]:
if hasattr(module, name):
obj = getattr(module, name)
if isinstance(obj, FastMCP | FastMCP1x):
return await self._resolve_factory(obj, file_path, name)
# No server found
logger.error(
f"No server object found in {file_path}. Please either:\n"
"1. Use a standard variable name (mcp, server, or app)\n"
"2. Specify the entrypoint name in fastmcp.json or use `file.py:object` syntax as your path.",
extra={"file": str(file_path)},
)
sys.exit(1)
async def _resolve_factory(self, obj: Any, file_path: Path, name: str) -> Any:
"""Resolve a server object or factory function to a server instance.
Args:
obj: The object that might be a server or factory function
file_path: Path to the file for error messages
name: Name of the object for error messages
Returns:
A server instance
"""
# Avoid circular import by importing here
from mcp.server.fastmcp import FastMCP as FastMCP1x
from fastmcp.server.server import FastMCP
# Check if it's a function or coroutine function
if inspect.isfunction(obj) or inspect.iscoroutinefunction(obj):
logger.debug(f"Found factory function '{name}' in {file_path}")
try:
if inspect.iscoroutinefunction(obj):
# Async factory function
server = await obj()
else:
# Sync factory function
server = obj()
# Validate the result is a FastMCP server
if not isinstance(server, FastMCP | FastMCP1x):
logger.error(
f"Factory function '{name}' must return a FastMCP server instance, "
f"got {type(server).__name__}",
extra={"file": str(file_path)},
)
sys.exit(1)
logger.debug(f"Factory function '{name}' created server: {server.name}")
return server
except Exception as e:
logger.error(
f"Failed to call factory function '{name}': {e}",
extra={"file": str(file_path)},
)
sys.exit(1)
# Not a function, return as-is (should be a server instance)
return obj

View file

@ -4,7 +4,7 @@ from __future__ import annotations
import importlib.metadata
from dataclasses import dataclass
from typing import Any
from typing import Any, cast
from mcp.server.fastmcp import FastMCP as FastMCP1x
@ -318,4 +318,4 @@ async def inspect_fastmcp(mcp: FastMCP[Any] | FastMCP1x) -> FastMCPInfo:
if isinstance(mcp, FastMCP1x):
return await inspect_fastmcp_v1(mcp)
else:
return await inspect_fastmcp_v2(mcp)
return await inspect_fastmcp_v2(cast(FastMCP[Any], mcp))

View file

@ -1,10 +1,16 @@
from typing import Any
from fastmcp.client.transports import ClientTransport
from fastmcp.client.transports import (
ClientTransport,
SSETransport,
StdioTransport,
StreamableHttpTransport,
)
from fastmcp.mcp_config import (
MCPConfig,
MCPServerTypes,
)
from fastmcp.server.proxy import FastMCPProxy, ProxyClient
from fastmcp.server.server import FastMCP
@ -23,6 +29,7 @@ def mcp_server_type_to_servers_and_transports(
mcp_server: MCPServerTypes,
) -> tuple[str, FastMCP[Any], ClientTransport]:
"""A utility function to convert each entry of an MCP Config into a transport and server."""
from fastmcp.mcp_config import (
TransformingRemoteMCPServer,
TransformingStdioMCPServer,
@ -31,10 +38,19 @@ def mcp_server_type_to_servers_and_transports(
server: FastMCP[Any]
transport: ClientTransport
client_name = ProxyClient.generate_name(f"MCP_{name}")
server_name = FastMCPProxy.generate_name(f"MCP_{name}")
if isinstance(mcp_server, TransformingRemoteMCPServer | TransformingStdioMCPServer):
server, transport = mcp_server._to_server_and_underlying_transport()
server, transport = mcp_server._to_server_and_underlying_transport(
server_name=server_name,
client_name=client_name,
)
else:
transport = mcp_server.to_transport()
server = FastMCP.as_proxy(backend=transport)
client: ProxyClient[StreamableHttpTransport | SSETransport | StdioTransport] = (
ProxyClient(transport=transport, name=client_name)
)
server = FastMCP.as_proxy(name=server_name, backend=client)
return name, server, transport

View file

@ -10,6 +10,7 @@ from pathlib import Path
from types import EllipsisType, UnionType
from typing import (
Annotated,
Any,
Protocol,
TypeAlias,
TypeVar,
@ -122,7 +123,7 @@ def issubclass_safe(cls: type, base: type) -> bool:
return False
def is_class_member_of_type(cls: type, base: type) -> bool:
def is_class_member_of_type(cls: Any, base: type) -> bool:
"""
Check if cls is a member of base, even if cls is a type variable.

View file

@ -4,7 +4,7 @@ from unittest.mock import Mock, patch
import pytest
from fastmcp.cli.cli import _build_uv_command, _parse_env_var, app
from fastmcp.cli.cli import _parse_env_var, app
class TestMainCLI:
@ -34,150 +34,6 @@ class TestMainCLI:
_parse_env_var("INVALID_FORMAT")
assert exc_info.value.code == 1
def test_build_uv_command_basic(self):
"""Test building basic uv command."""
cmd = _build_uv_command("server.py")
expected = ["uv", "run", "--with", "fastmcp", "fastmcp", "run", "server.py"]
assert cmd == expected
def test_build_uv_command_with_editable(self):
"""Test building uv command with editable package."""
editable_path = Path("/path/to/package")
cmd = _build_uv_command("server.py", with_editable=editable_path)
expected = [
"uv",
"run",
"--with",
"fastmcp",
"--with-editable",
str(editable_path),
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
def test_build_uv_command_with_packages(self):
"""Test building uv command with additional packages."""
cmd = _build_uv_command("server.py", with_packages=["pkg1", "pkg2"])
expected = [
"uv",
"run",
"--with",
"fastmcp",
"--with",
"pkg1",
"--with",
"pkg2",
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
def test_build_uv_command_no_banner(self):
"""Test building uv command with no banner flag."""
cmd = _build_uv_command("server.py", no_banner=True)
expected = [
"uv",
"run",
"--with",
"fastmcp",
"fastmcp",
"run",
"server.py",
"--no-banner",
]
assert cmd == expected
def test_build_uv_command_with_python_version(self):
"""Test building uv command with Python version."""
cmd = _build_uv_command("server.py", python_version="3.11")
expected = [
"uv",
"run",
"--python",
"3.11",
"--with",
"fastmcp",
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
def test_build_uv_command_with_project(self):
"""Test building uv command with project directory."""
project_path = Path("/path/to/project")
cmd = _build_uv_command("server.py", project=project_path)
expected = [
"uv",
"run",
"--project",
str(project_path),
"--with",
"fastmcp",
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
def test_build_uv_command_with_requirements(self):
"""Test building uv command with requirements file."""
req_path = Path("requirements.txt")
cmd = _build_uv_command("server.py", with_requirements=req_path)
expected = [
"uv",
"run",
"--with",
"fastmcp",
"--with-requirements",
"requirements.txt",
"fastmcp",
"run",
"server.py",
]
assert cmd == expected
def test_build_uv_command_with_all_options(self):
"""Test building uv command with all options."""
project_path = Path("/my/project")
editable_path = Path("/local/pkg")
requirements_path = Path("reqs.txt")
cmd = _build_uv_command(
"server.py",
python_version="3.10",
project=project_path,
with_packages=["pandas", "numpy"],
with_requirements=requirements_path,
with_editable=editable_path,
no_banner=True,
)
expected = [
"uv",
"run",
"--python",
"3.10",
"--project",
str(project_path),
"--with",
"fastmcp",
"--with-editable",
str(editable_path),
"--with",
"pandas",
"--with",
"numpy",
"--with-requirements",
str(requirements_path),
"fastmcp",
"run",
"server.py",
"--no-banner",
]
assert cmd == expected
class TestVersionCommand:
"""Test the version command."""
@ -472,6 +328,47 @@ class TestRunCommand:
]
)
def test_run_command_parsing_skip_env_flag(self):
"""Test run command parsing with --skip-env flag."""
command, bound, _ = app.parse_args(
[
"run",
"server.py",
"--skip-env",
]
)
assert command is not None
assert bound.arguments["server_spec"] == "server.py"
assert bound.arguments["skip_env"] is True
def test_run_command_parsing_skip_source_flag(self):
"""Test run command parsing with --skip-source flag."""
command, bound, _ = app.parse_args(
[
"run",
"server.py",
"--skip-source",
]
)
assert command is not None
assert bound.arguments["server_spec"] == "server.py"
assert bound.arguments["skip_source"] is True
def test_run_command_parsing_both_skip_flags(self):
"""Test run command parsing with both --skip-env and --skip-source flags."""
command, bound, _ = app.parse_args(
[
"run",
"server.py",
"--skip-env",
"--skip-source",
]
)
assert command is not None
assert bound.arguments["server_spec"] == "server.py"
assert bound.arguments["skip_env"] is True
assert bound.arguments["skip_source"] is True
class TestWindowsSpecific:
"""Test Windows-specific functionality."""
@ -557,22 +454,27 @@ class TestWindowsSpecific:
def test_windows_path_parsing_with_colon(self, tmp_path):
"""Test parsing Windows paths with drive letters and colons."""
from fastmcp.cli.run import parse_file_path
from pathlib import Path
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import (
FileSystemSource,
)
# Create a real test file to test the logic
test_file = tmp_path / "server.py"
test_file.write_text("# test server")
# Test normal file parsing (works on all platforms)
file_path, obj = parse_file_path(str(test_file))
assert obj is None
source = FileSystemSource(path=str(test_file))
assert source.entrypoint is None
assert Path(source.path).resolve() == test_file.resolve()
# Test file:object parsing
file_path, obj = parse_file_path(f"{test_file}:myapp")
assert obj == "myapp"
source = FileSystemSource(path=f"{test_file}:myapp")
assert source.entrypoint == "myapp"
# Test that the file portion resolves correctly when object is specified
assert file_path == test_file.resolve()
assert Path(source.path).resolve() == test_file.resolve()
class TestInspectCommand:

View file

@ -8,77 +8,52 @@ import pytest
from pydantic import ValidationError
from fastmcp.utilities.fastmcp_config import (
DeploymentConfig,
EntrypointConfig,
EnvironmentConfig,
Deployment,
Environment,
FastMCPConfig,
)
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
class TestEntrypointConfig:
"""Test EntrypointConfig class."""
class TestFileSystemSource:
"""Test FileSystemSource class."""
def test_string_entrypoint(self):
"""Test that string entrypoint is converted to EntrypointConfig."""
config = FastMCPConfig(entrypoint="server.py")
# With the new validator, this should be converted to EntrypointConfig
assert isinstance(config.entrypoint, EntrypointConfig)
assert config.entrypoint.file == "server.py"
assert config.entrypoint.object is None
def test_dict_source_minimal(self):
"""Test that dict source is converted to FileSystemSource."""
config = FastMCPConfig(source={"path": "server.py"})
# Dict is converted to FileSystemSource
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
assert config.source.entrypoint is None
assert config.source.type == "filesystem"
# get_entrypoint should return the same object
entrypoint = config.get_entrypoint()
assert isinstance(entrypoint, EntrypointConfig)
assert entrypoint.file == "server.py"
assert entrypoint.object is None
def test_dict_source_with_entrypoint(self):
"""Test dict source with entrypoint field."""
config = FastMCPConfig(source={"path": "server.py", "entrypoint": "app"})
# Dict with entrypoint is converted to FileSystemSource
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
assert config.source.entrypoint == "app"
assert config.source.type == "filesystem"
def test_string_entrypoint_with_object(self):
"""Test string entrypoint with :object syntax."""
config = FastMCPConfig(entrypoint="server.py:app")
# With the new validator, this should be converted to EntrypointConfig
assert isinstance(config.entrypoint, EntrypointConfig)
assert config.entrypoint.file == "server.py"
assert config.entrypoint.object == "app"
# get_entrypoint should return the same object
entrypoint = config.get_entrypoint()
assert isinstance(entrypoint, EntrypointConfig)
assert entrypoint.file == "server.py"
assert entrypoint.object == "app"
def test_object_entrypoint(self):
"""Test EntrypointConfig object format."""
def test_filesystem_source_entrypoint(self):
"""Test FileSystemSource entrypoint format."""
config = FastMCPConfig(
entrypoint=EntrypointConfig(file="src/server.py", object="mcp")
source=FileSystemSource(path="src/server.py", entrypoint="mcp")
)
assert isinstance(config.entrypoint, EntrypointConfig)
assert config.entrypoint.file == "src/server.py"
assert config.entrypoint.object == "mcp"
def test_get_entrypoint_path_resolution(self, tmp_path):
"""Test that get_entrypoint resolves paths relative to config file."""
config_dir = tmp_path / "config"
config_dir.mkdir()
server_dir = tmp_path / "src"
server_dir.mkdir()
server_file = server_dir / "server.py"
server_file.write_text("# server")
config = FastMCPConfig(entrypoint="../src/server.py")
entrypoint = config.get_entrypoint(config_dir / "fastmcp.json")
# Should resolve to absolute path
assert Path(entrypoint.file).is_absolute()
assert Path(entrypoint.file) == server_file.resolve()
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "src/server.py"
assert config.source.entrypoint == "mcp"
assert config.source.type == "filesystem"
class TestEnvironmentConfig:
"""Test EnvironmentConfig class."""
class TestEnvironment:
"""Test Environment class."""
def test_environment_config_fields(self):
"""Test all EnvironmentConfig fields."""
"""Test all Environment fields."""
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
environment={
"python": "3.12",
"dependencies": ["requests", "numpy>=2.0"],
@ -98,27 +73,29 @@ class TestEnvironmentConfig:
def test_needs_uv(self):
"""Test needs_uv() method."""
# No environment config - doesn't need UV
config = FastMCPConfig(entrypoint="server.py")
config = FastMCPConfig(source={"path": "server.py"})
assert not config.environment.needs_uv()
# Empty environment - doesn't need UV
config = FastMCPConfig(entrypoint="server.py", environment={})
config = FastMCPConfig(source={"path": "server.py"}, environment={})
assert not config.environment.needs_uv()
# With dependencies - needs UV
config = FastMCPConfig(
entrypoint="server.py", environment={"dependencies": ["requests"]}
source={"path": "server.py"}, environment={"dependencies": ["requests"]}
)
assert config.environment.needs_uv()
# With Python version - needs UV
config = FastMCPConfig(entrypoint="server.py", environment={"python": "3.12"})
config = FastMCPConfig(
source={"path": "server.py"}, environment={"python": "3.12"}
)
assert config.environment.needs_uv()
def test_build_uv_args(self):
"""Test build_uv_args() method."""
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
environment={
"python": "3.12",
"dependencies": ["requests", "numpy"],
@ -143,33 +120,10 @@ class TestEnvironmentConfig:
assert "run" in args[-2:]
assert "server.py" in args[-1:]
def test_merge_with_cli_args(self):
"""Test merge_with_cli_args() method."""
config = FastMCPConfig(
entrypoint="server.py",
environment={
"python": "3.11",
"dependencies": ["requests"],
},
)
# CLI args should take precedence
merged = config.environment.merge_with_cli_args(
python="3.12", # Override
with_packages=["numpy"], # Add to dependencies
with_requirements=None,
project=None,
)
assert merged["python"] == "3.12" # CLI override
assert set(merged["with_packages"]) == {"requests", "numpy"} # Merged
assert merged["with_requirements"] is None
assert merged["project"] is None
def test_run_with_uv(self):
"""Test run_with_uv() subprocess execution."""
config = FastMCPConfig(
entrypoint="server.py", environment={"dependencies": ["requests"]}
source={"path": "server.py"}, environment={"dependencies": ["requests"]}
)
# run_with_uv calls sys.exit, so we expect SystemExit
@ -182,13 +136,13 @@ class TestEnvironmentConfig:
assert exc_info.value.code == 1
class TestDeploymentConfig:
"""Test DeploymentConfig class."""
class TestDeployment:
"""Test Deployment class."""
def test_deployment_config_fields(self):
"""Test all DeploymentConfig fields."""
"""Test all Deployment fields."""
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={
"transport": "http",
"host": "0.0.0.0",
@ -211,33 +165,6 @@ class TestDeploymentConfig:
assert deploy.cwd == "./work"
assert deploy.args == ["--debug"]
def test_merge_with_cli_args(self):
"""Test DeploymentConfig merge_with_cli_args() method."""
config = FastMCPConfig(
entrypoint="server.py",
deployment={
"transport": "stdio",
"port": 3000,
"log_level": "INFO",
},
)
# CLI args should take precedence
merged = config.deployment.merge_with_cli_args(
transport="http", # Override
host="localhost", # New value
port=None, # Keep config value
path=None,
log_level="DEBUG", # Override
server_args=["--test"],
)
assert merged["transport"] == "http" # CLI override
assert merged["host"] == "localhost" # CLI value
assert merged["port"] == 3000 # Config value (CLI was None)
assert merged["log_level"] == "DEBUG" # CLI override
assert merged["server_args"] == ["--test"] # CLI value
def test_apply_runtime_settings(self, tmp_path):
"""Test apply_runtime_settings() method."""
import os
@ -247,7 +174,7 @@ class TestDeploymentConfig:
work_dir.mkdir()
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={
"env": {"TEST_VAR": "test_value"},
"cwd": "work",
@ -283,7 +210,7 @@ class TestDeploymentConfig:
os.environ["ENV_NAME"] = "production"
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={
"env": {
"API_URL": "https://api.${BASE_URL}/v1",
@ -328,24 +255,24 @@ class TestFastMCPConfig:
def test_minimal_config(self):
"""Test creating a config with only required fields."""
config = FastMCPConfig(entrypoint="server.py")
assert isinstance(config.entrypoint, EntrypointConfig)
assert config.entrypoint.file == "server.py"
assert config.entrypoint.object is None
config = FastMCPConfig(source={"path": "server.py"})
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
assert config.source.entrypoint is None
# Environment and deployment are now always present but empty
assert isinstance(config.environment, EnvironmentConfig)
assert isinstance(config.deployment, DeploymentConfig)
assert isinstance(config.environment, Environment)
assert isinstance(config.deployment, Deployment)
# Check they have no values set
assert not config.environment.needs_uv()
assert all(
getattr(config.deployment, field, None) is None
for field in DeploymentConfig.model_fields
for field in Deployment.model_fields
)
def test_nested_structure(self):
"""Test the nested configuration structure."""
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
environment={
"python": "3.12",
"dependencies": ["fastmcp"],
@ -356,17 +283,17 @@ class TestFastMCPConfig:
},
)
assert isinstance(config.entrypoint, EntrypointConfig)
assert config.entrypoint.file == "server.py"
assert config.entrypoint.object is None
assert isinstance(config.environment, EnvironmentConfig)
assert isinstance(config.deployment, DeploymentConfig)
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
assert config.source.entrypoint is None
assert isinstance(config.environment, Environment)
assert isinstance(config.deployment, Deployment)
def test_from_file(self, tmp_path):
"""Test loading config from JSON file with nested structure."""
config_data = {
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": {"file": "src/server.py", "object": "app"},
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {"path": "src/server.py", "entrypoint": "app"},
"environment": {"python": "3.12", "dependencies": ["requests"]},
"deployment": {"transport": "http", "port": 8000},
}
@ -376,19 +303,19 @@ class TestFastMCPConfig:
config = FastMCPConfig.from_file(config_file)
# When loaded from JSON with object format, it becomes EntrypointConfig
assert isinstance(config.entrypoint, EntrypointConfig)
assert config.entrypoint.file == "src/server.py"
assert config.entrypoint.object == "app"
# When loaded from JSON with entrypoint format, it becomes EntrypointConfig
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "src/server.py"
assert config.source.entrypoint == "app"
assert config.environment.python == "3.12"
assert config.environment.dependencies == ["requests"]
assert config.deployment.transport == "http"
assert config.deployment.port == 8000
def test_from_file_with_string_entrypoint(self, tmp_path):
"""Test loading config with string entrypoint."""
"""Test loading config with dict source format."""
config_data = {
"entrypoint": "server.py:mcp",
"source": {"path": "server.py", "entrypoint": "mcp"},
"environment": {"dependencies": ["fastmcp"]},
}
@ -397,19 +324,14 @@ class TestFastMCPConfig:
config = FastMCPConfig.from_file(config_file)
# String entrypoint with : should be converted to EntrypointConfig
assert isinstance(config.entrypoint, EntrypointConfig)
assert config.entrypoint.file == "server.py"
assert config.entrypoint.object == "mcp"
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
assert config.source.entrypoint == "mcp"
# get_entrypoint should return the same
entrypoint = config.get_entrypoint()
assert entrypoint.file == "server.py"
assert entrypoint.object == "mcp"
def test_string_entrypoint_with_object_and_environment(self, tmp_path):
"""Test that file.py:object syntax works with environment config."""
def test_string_entrypoint_with_entrypoint_and_environment(self, tmp_path):
"""Test that file.py:entrypoint syntax works with environment config."""
config_data = {
"entrypoint": "src/server.py:app",
"source": {"path": "src/server.py", "entrypoint": "app"},
"environment": {"python": "3.12", "dependencies": ["fastmcp", "requests"]},
"deployment": {"transport": "http", "port": 8000},
}
@ -420,9 +342,9 @@ class TestFastMCPConfig:
config = FastMCPConfig.from_file(config_file)
# Should be parsed into EntrypointConfig
assert isinstance(config.entrypoint, EntrypointConfig)
assert config.entrypoint.file == "src/server.py"
assert config.entrypoint.object == "app"
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "src/server.py"
assert config.source.entrypoint == "app"
# Environment config should still work
assert config.environment.python == "3.12"
@ -435,7 +357,7 @@ class TestFastMCPConfig:
def test_find_config_in_current_dir(self, tmp_path):
"""Test finding config in current directory."""
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps({"entrypoint": "server.py"}))
config_file.write_text(json.dumps({"source": {"path": "server.py"}}))
original_cwd = os.getcwd()
try:
@ -448,7 +370,7 @@ class TestFastMCPConfig:
def test_find_config_not_in_parent_dir(self, tmp_path):
"""Test that config is NOT found in parent directory."""
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps({"entrypoint": "server.py"}))
config_file.write_text(json.dumps({"source": {"path": "server.py"}}))
subdir = tmp_path / "subdir"
subdir.mkdir()
@ -460,7 +382,7 @@ class TestFastMCPConfig:
def test_find_config_in_specified_dir(self, tmp_path):
"""Test finding config in the specified directory."""
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps({"entrypoint": "server.py"}))
config_file.write_text(json.dumps({"source": {"path": "server.py"}}))
# Should find config when looking in the directory that contains it
found = FastMCPConfig.find_config(tmp_path)
@ -474,7 +396,7 @@ class TestFastMCPConfig:
def test_invalid_transport(self, tmp_path):
"""Test loading config with invalid transport value."""
config_data = {
"entrypoint": "server.py",
"source": {"path": "server.py"},
"deployment": {"transport": "invalid_transport"},
}
@ -485,29 +407,33 @@ class TestFastMCPConfig:
FastMCPConfig.from_file(config_file)
def test_optional_sections(self):
"""Test that all config sections are optional except entrypoint."""
# Only entrypoint is required
config = FastMCPConfig(entrypoint="server.py")
assert isinstance(config.entrypoint, EntrypointConfig)
assert config.entrypoint.file == "server.py"
"""Test that all config sections are optional except source."""
# Only source is required
config = FastMCPConfig(source={"path": "server.py"})
assert isinstance(config.source, FileSystemSource)
assert config.source.path == "server.py"
# Environment and deployment are now always present but may be empty
assert isinstance(config.environment, EnvironmentConfig)
assert isinstance(config.deployment, DeploymentConfig)
assert isinstance(config.environment, Environment)
assert isinstance(config.deployment, Deployment)
# Only environment with values
config = FastMCPConfig(entrypoint="server.py", environment={"python": "3.12"})
config = FastMCPConfig(
source={"path": "server.py"}, environment={"python": "3.12"}
)
assert config.environment.python == "3.12"
assert isinstance(config.deployment, DeploymentConfig)
assert isinstance(config.deployment, Deployment)
assert all(
getattr(config.deployment, field, None) is None
for field in DeploymentConfig.model_fields
for field in Deployment.model_fields
)
# Only deployment with values
config = FastMCPConfig(entrypoint="server.py", deployment={"transport": "http"})
assert isinstance(config.environment, EnvironmentConfig)
config = FastMCPConfig(
source={"path": "server.py"}, deployment={"transport": "http"}
)
assert isinstance(config.environment, Environment)
assert all(
getattr(config.environment, field, None) is None
for field in EnvironmentConfig.model_fields
for field in Environment.model_fields
)
assert config.deployment.transport == "http"

View file

@ -239,11 +239,14 @@ class TestInstallCursor:
"""Test cursor installation with editable package."""
mock_open_deeplink.return_value = True
# Use an absolute path that works on all platforms
editable_path = Path.cwd() / "local" / "package"
result = install_cursor(
file=Path("/path/to/server.py"),
server_object="custom_app",
name="test-server",
with_editable=Path("/local/package"),
with_editable=editable_path,
)
assert result is True
@ -255,9 +258,10 @@ class TestInstallCursor:
config_data = json.loads(decoded)
assert "--with-editable" in config_data["args"]
# Check for the editable path in a platform-agnostic way
editable_path_str = str(Path("/local/package"))
assert editable_path_str in config_data["args"]
# Check that the path was resolved (should be absolute)
editable_idx = config_data["args"].index("--with-editable") + 1
resolved_path = config_data["args"][editable_idx]
assert Path(resolved_path).is_absolute()
assert "server.py:custom_app" in " ".join(config_data["args"])
@patch("fastmcp.cli.install.cursor.open_deeplink")
@ -276,7 +280,11 @@ class TestInstallCursor:
# Verify failure message was printed
mock_print.assert_called()
def test_install_cursor_deduplicate_packages(self):
@patch(
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.Environment._find_fastmcp_dev_path",
return_value=None, # Mock to disable dev mode so "fastmcp" count is predictable
)
def test_install_cursor_deduplicate_packages(self, mock_find_dev):
"""Test that duplicate packages are deduplicated."""
with patch("fastmcp.cli.install.cursor.open_deeplink") as mock_open:
mock_open.return_value = True

View file

@ -37,8 +37,8 @@ if __name__ == "__main__":
# Create config file
config_data = {
"$schema": "https://gofastmcp.com/schemas/fastmcp_config/v1.json",
"entrypoint": "server.py",
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {"path": "server.py"},
"environment": {
"python": sys.version.split()[0], # Use current Python version
"dependencies": ["fastmcp"],
@ -58,7 +58,7 @@ class TestConfigFileDetection:
def test_detect_standard_fastmcp_json(self, tmp_path):
"""Test detection of standard fastmcp.json file."""
config_file = tmp_path / "fastmcp.json"
config_file.write_text(json.dumps({"entrypoint": "server.py"}))
config_file.write_text(json.dumps({"source": {"path": "server.py"}}))
# Should be detected as fastmcp config
assert "fastmcp.json" in config_file.name
@ -67,7 +67,7 @@ class TestConfigFileDetection:
def test_detect_prefixed_fastmcp_json(self, tmp_path):
"""Test detection of prefixed fastmcp.json files."""
config_file = tmp_path / "my.fastmcp.json"
config_file.write_text(json.dumps({"entrypoint": "server.py"}))
config_file.write_text(json.dumps({"source": {"path": "server.py"}}))
# Should be detected as fastmcp config
assert "fastmcp.json" in config_file.name
@ -75,7 +75,7 @@ class TestConfigFileDetection:
def test_detect_test_fastmcp_json(self, tmp_path):
"""Test detection of test_fastmcp.json file."""
config_file = tmp_path / "test_fastmcp.json"
config_file.write_text(json.dumps({"entrypoint": "server.py"}))
config_file.write_text(json.dumps({"source": {"path": "server.py"}}))
# Should be detected as fastmcp config
assert "fastmcp.json" in config_file.name
@ -91,14 +91,18 @@ class TestConfigWithClient:
config_file = server_with_config / "fastmcp.json"
config = FastMCPConfig.from_file(config_file)
# Import the server using the entrypoint
# Import the server using the source
import importlib.util
import sys
entrypoint = config.get_entrypoint(config_file)
spec = importlib.util.spec_from_file_location("test_server", entrypoint.file)
# Resolve the path from the source
source_path = Path(config.source.path)
if not source_path.is_absolute():
source_path = (config_file.parent / source_path).resolve()
spec = importlib.util.spec_from_file_location("test_server", str(source_path))
if spec is None or spec.loader is None:
raise RuntimeError(f"Could not load module from {entrypoint.file}")
raise RuntimeError(f"Could not load module from {source_path}")
module = importlib.util.module_from_spec(spec)
sys.modules["test_server"] = module
spec.loader.exec_module(module)
@ -129,7 +133,7 @@ class TestEnvironmentExecution:
def test_needs_uv_with_dependencies(self):
"""Test that environment with dependencies needs UV."""
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
environment={"dependencies": ["requests", "numpy"]}, # type: ignore[arg-type]
)
@ -139,7 +143,7 @@ class TestEnvironmentExecution:
def test_needs_uv_with_python_version(self):
"""Test that environment with Python version needs UV."""
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
environment={"python": "3.12"}, # type: ignore[arg-type]
)
@ -148,7 +152,7 @@ class TestEnvironmentExecution:
def test_no_uv_needed_without_environment(self):
"""Test that no UV is needed without environment config."""
config = FastMCPConfig(entrypoint="server.py")
config = FastMCPConfig(source={"path": "server.py"})
# Environment is now always present but may be empty
assert config.environment is not None
@ -157,7 +161,7 @@ class TestEnvironmentExecution:
def test_no_uv_needed_with_empty_environment(self):
"""Test that no UV is needed with empty environment config."""
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
environment={}, # type: ignore[arg-type]
)
@ -165,77 +169,11 @@ class TestEnvironmentExecution:
assert not config.environment.needs_uv()
class TestCLIArgumentMerging:
"""Test CLI argument merging with config values."""
def test_cli_overrides_environment(self):
"""Test that CLI args override environment config."""
config = FastMCPConfig(
entrypoint="server.py",
environment={"python": "3.11", "dependencies": ["requests"]}, # type: ignore[arg-type]
)
assert config.environment is not None
merged = config.environment.merge_with_cli_args(
python="3.12", # Override Python version
with_packages=["numpy"], # Add package
with_requirements=None,
project=None,
)
assert merged["python"] == "3.12" # CLI wins
assert "requests" in merged["with_packages"] # From config
assert "numpy" in merged["with_packages"] # From CLI
def test_cli_overrides_deployment(self):
"""Test that CLI args override deployment config."""
config = FastMCPConfig(
entrypoint="server.py",
deployment={"transport": "stdio", "port": 3000, "log_level": "INFO"}, # type: ignore[arg-type]
)
assert config.deployment is not None
merged = config.deployment.merge_with_cli_args(
transport="http", # Override transport
host="localhost", # New value
port=8080, # Override port
path=None,
log_level="DEBUG", # Override log level
server_args=None,
)
assert merged["transport"] == "http" # CLI wins
assert merged["host"] == "localhost" # CLI value
assert merged["port"] == 8080 # CLI wins
assert merged["log_level"] == "DEBUG" # CLI wins
def test_config_values_when_cli_is_none(self):
"""Test that config values are used when CLI args are None."""
config = FastMCPConfig(
entrypoint="server.py",
deployment={"transport": "http", "port": 3000}, # type: ignore[arg-type]
)
assert config.deployment is not None
merged = config.deployment.merge_with_cli_args(
transport=None, # Use config
host=None, # No value
port=None, # Use config
path=None,
log_level=None,
server_args=None,
)
assert merged["transport"] == "http" # From config
assert merged["port"] == 3000 # From config
assert merged["host"] is None # No value provided
class TestPathResolution:
"""Test path resolution in configurations."""
def test_entrypoint_path_resolution(self, tmp_path):
"""Test that entrypoint paths are resolved relative to config."""
def test_source_path_resolution(self, tmp_path):
"""Test that source paths are resolved relative to config."""
# Create nested directory structure
config_dir = tmp_path / "config"
config_dir.mkdir()
@ -246,14 +184,11 @@ class TestPathResolution:
server_file = src_dir / "server.py"
server_file.write_text("# Server")
config = FastMCPConfig(entrypoint="../src/server.py")
config = FastMCPConfig(source={"path": "../src/server.py"})
# Get entrypoint resolved relative to config location
config_file = config_dir / "fastmcp.json"
entrypoint = config.get_entrypoint(config_file)
# Should resolve to absolute path of server file
assert Path(entrypoint.file) == server_file.resolve()
# The source path is resolved during load_server
# For now, just check that the source is created correctly
assert config.source.path == "../src/server.py"
def test_cwd_path_resolution(self, tmp_path):
"""Test that working directory is resolved relative to config."""
@ -264,7 +199,7 @@ class TestPathResolution:
work_dir.mkdir()
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={"cwd": "work"}, # type: ignore[arg-type]
)
@ -288,7 +223,7 @@ class TestPathResolution:
reqs_file.write_text("fastmcp>=2.0")
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
environment={"requirements": "requirements.txt"}, # type: ignore[arg-type]
)
@ -309,7 +244,7 @@ class TestConfigValidation:
"""Test that invalid transport values are rejected."""
with pytest.raises(ValueError):
FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={"transport": "invalid_transport"}, # type: ignore[arg-type]
)
@ -317,7 +252,7 @@ class TestConfigValidation:
"""Test that streamable-http transport is rejected in fastmcp.json config."""
with pytest.raises(ValueError):
FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={"transport": "streamable-http"}, # type: ignore[arg-type]
)
@ -325,12 +260,12 @@ class TestConfigValidation:
"""Test that invalid log level values are rejected."""
with pytest.raises(ValueError):
FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={"log_level": "INVALID"}, # type: ignore[arg-type]
)
def test_missing_entrypoint_rejected(self):
"""Test that config without entrypoint is rejected."""
def test_missing_source_rejected(self):
"""Test that config without source is rejected."""
with pytest.raises(ValueError):
FastMCPConfig() # type: ignore[call-arg]
@ -338,7 +273,7 @@ class TestConfigValidation:
"""Test that all valid transport values are accepted."""
for transport in ["stdio", "http", "sse"]:
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={"transport": transport}, # type: ignore[arg-type]
)
assert config.deployment is not None
@ -348,7 +283,7 @@ class TestConfigValidation:
"""Test that all valid log levels are accepted."""
for level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
config = FastMCPConfig(
entrypoint="server.py",
source={"path": "server.py"},
deployment={"log_level": level}, # type: ignore[arg-type]
)
assert config.deployment is not None

View file

@ -40,10 +40,11 @@ def test_schema_has_correct_id():
"""Test that the schema has the correct $id field."""
generated_schema = generate_schema()
assert generated_schema is not None
assert "$id" in generated_schema
assert (
generated_schema["$id"]
== "https://gofastmcp.com/schemas/fastmcp_config/v1.json"
== "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json"
)
@ -51,19 +52,21 @@ def test_schema_has_required_fields():
"""Test that the schema specifies the required fields correctly."""
generated_schema = generate_schema()
# Check that entrypoint is required
assert generated_schema is not None
# Check that source is required
assert "required" in generated_schema
assert "entrypoint" in generated_schema["required"]
assert "source" in generated_schema["required"]
# Check that entrypoint is in properties
# Check that source is in properties
assert "properties" in generated_schema
assert "entrypoint" in generated_schema["properties"]
assert "source" in generated_schema["properties"]
def test_schema_nested_structure():
"""Test that the schema has the correct nested structure."""
generated_schema = generate_schema()
assert generated_schema is not None
properties = generated_schema["properties"]
# Check environment section
@ -95,6 +98,7 @@ def test_schema_transport_enum():
"""Test that transport field has correct enum values."""
generated_schema = generate_schema()
assert generated_schema is not None
# Navigate to transport field
deploy_schema = generated_schema["properties"]["deployment"]
@ -129,6 +133,7 @@ def test_schema_log_level_enum():
"""Test that log_level field has correct enum values."""
generated_schema = generate_schema()
assert generated_schema is not None
# Navigate to log_level field
deploy_schema = generated_schema["properties"]["deployment"]

View file

@ -7,14 +7,13 @@ from pydantic import ValidationError
from fastmcp.cli.run import (
create_mcp_config_server,
import_server,
is_url,
parse_file_path,
)
from fastmcp.client.client import Client
from fastmcp.client.transports import FastMCPTransport
from fastmcp.mcp_config import MCPConfig, StdioMCPServer
from fastmcp.server.server import FastMCP
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
class TestUrlDetection:
@ -41,52 +40,54 @@ class TestUrlDetection:
assert not is_url("file:///path/to/file")
class TestFilePathParsing:
"""Test file path parsing functionality."""
class TestFileSystemSource:
"""Test FileSystemSource path parsing functionality."""
def test_parse_file_path_simple(self, tmp_path):
def test_parse_simple_path(self, tmp_path):
"""Test parsing simple file path without object."""
test_file = tmp_path / "server.py"
test_file.write_text("# test server")
file_path, server_object = parse_file_path(str(test_file))
assert file_path == test_file.resolve()
assert server_object is None
source = FileSystemSource(path=str(test_file))
assert Path(source.path).resolve() == test_file.resolve()
assert source.entrypoint is None
def test_parse_file_path_with_object(self, tmp_path):
def test_parse_path_with_object(self, tmp_path):
"""Test parsing file path with object specification."""
test_file = tmp_path / "server.py"
test_file.write_text("# test server")
file_path, server_object = parse_file_path(f"{test_file}:app")
assert file_path == test_file.resolve()
assert server_object == "app"
source = FileSystemSource(path=f"{test_file}:app")
assert Path(source.path).resolve() == test_file.resolve()
assert source.entrypoint == "app"
def test_parse_file_path_complex_object(self, tmp_path):
def test_parse_complex_object(self, tmp_path):
"""Test parsing file path with complex object specification."""
test_file = tmp_path / "server.py"
test_file.write_text("# test server")
# The current implementation splits on the last colon, so file:module:app
# becomes file_path="file:module" and server_object="app"
# The implementation splits on the last colon, so file:module:app
# becomes file_path="file:module" and entrypoint="app"
# We need to create a file with a colon in the name for this test
complex_file = tmp_path / "server:module.py"
complex_file.write_text("# test server")
file_path, server_object = parse_file_path(f"{complex_file}:app")
assert file_path == complex_file.resolve()
assert server_object == "app"
source = FileSystemSource(path=f"{complex_file}:app")
assert Path(source.path).resolve() == complex_file.resolve()
assert source.entrypoint == "app"
def test_parse_file_path_nonexistent(self):
"""Test parsing nonexistent file path exits."""
async def test_load_server_nonexistent(self):
"""Test loading nonexistent file path exits."""
source = FileSystemSource(path="nonexistent.py")
with pytest.raises(SystemExit) as exc_info:
parse_file_path("nonexistent.py")
await source.load_server()
assert exc_info.value.code == 1
def test_parse_file_path_directory(self, tmp_path):
"""Test parsing directory path exits."""
async def test_load_server_directory(self, tmp_path):
"""Test loading directory path exits."""
source = FileSystemSource(path=str(tmp_path))
with pytest.raises(SystemExit) as exc_info:
parse_file_path(str(tmp_path))
await source.load_server()
assert exc_info.value.code == 1
@ -157,7 +158,8 @@ def greet(name: str) -> str:
return f"Hello, {name}!"
""")
server = await import_server(test_file)
source = FileSystemSource(path=str(test_file))
server = await source.load_server()
assert server.name == "TestServer"
tools = await server.get_tools()
assert "greet" in tools
@ -178,7 +180,8 @@ if __name__ == "__main__":
app.run()
""")
server = await import_server(test_file)
source = FileSystemSource(path=str(test_file))
server = await source.load_server()
assert server.name == "MainServer"
tools = await server.get_tools()
assert "calculate" in tools
@ -192,7 +195,8 @@ import fastmcp
mcp = fastmcp.FastMCP("MCPServer")
""")
server = await import_server(mcp_file)
source = FileSystemSource(path=str(mcp_file))
server = await source.load_server()
assert server.name == "MCPServer"
# Test with 'server' name
@ -202,7 +206,8 @@ import fastmcp
server = fastmcp.FastMCP("ServerServer")
""")
server = await import_server(server_file)
source = FileSystemSource(path=str(server_file))
server = await source.load_server()
assert server.name == "ServerServer"
# Test with 'app' name
@ -212,7 +217,8 @@ import fastmcp
app = fastmcp.FastMCP("AppServer")
""")
server = await import_server(app_file)
source = FileSystemSource(path=str(app_file))
server = await source.load_server()
assert server.name == "AppServer"
async def test_import_server_nonstandard_name(self, tmp_path):
@ -228,7 +234,8 @@ def custom_tool() -> str:
return "custom"
""")
server = await import_server(test_file, "my_custom_server")
source = FileSystemSource(path=f"{test_file}:my_custom_server")
server = await source.load_server()
assert server.name == "CustomServer"
tools = await server.get_tools()
assert "custom_tool" in tools
@ -242,8 +249,9 @@ import fastmcp
other_name = fastmcp.FastMCP("OtherServer")
""")
source = FileSystemSource(path=str(test_file))
with pytest.raises(SystemExit) as exc_info:
await import_server(test_file)
await source.load_server()
assert exc_info.value.code == 1
async def test_import_server_nonexistent_object_fails(self, tmp_path):
@ -255,6 +263,131 @@ import fastmcp
mcp = fastmcp.FastMCP("TestServer")
""")
source = FileSystemSource(path=f"{test_file}:nonexistent")
with pytest.raises(SystemExit) as exc_info:
await import_server(test_file, "nonexistent")
await source.load_server()
assert exc_info.value.code == 1
class TestSkipSource:
"""Test the --skip-source functionality."""
async def test_run_command_calls_prepare_by_default(self, tmp_path):
"""Test that run_command calls source.prepare() by default."""
from unittest.mock import AsyncMock, patch
from fastmcp.cli.run import run_command
# Create a test server file
test_file = tmp_path / "server.py"
test_file.write_text("""
import fastmcp
mcp = fastmcp.FastMCP("TestServer")
""")
# Create a test config file
config_file = tmp_path / "fastmcp.json"
config_data = {"source": {"path": str(test_file), "entrypoint": "mcp"}}
config_file.write_text(json.dumps(config_data))
# Mock the prepare method and server run
with (
patch.object(
FileSystemSource, "prepare", new_callable=AsyncMock
) as prepare_mock,
patch("fastmcp.server.server.FastMCP.run_async", new_callable=AsyncMock),
):
# Run the command
await run_command(str(config_file))
# Verify prepare was called
prepare_mock.assert_called_once()
async def test_run_command_skips_prepare_with_flag(self, tmp_path):
"""Test that run_command skips source.prepare() when skip_source=True."""
from unittest.mock import AsyncMock, patch
from fastmcp.cli.run import run_command
# Create a test server file
test_file = tmp_path / "server.py"
test_file.write_text("""
import fastmcp
mcp = fastmcp.FastMCP("TestServer")
""")
# Create a test config file
config_file = tmp_path / "fastmcp.json"
config_data = {"source": {"path": str(test_file), "entrypoint": "mcp"}}
config_file.write_text(json.dumps(config_data))
# Mock the prepare method and server run
with (
patch.object(
FileSystemSource, "prepare", new_callable=AsyncMock
) as prepare_mock,
patch("fastmcp.server.server.FastMCP.run_async", new_callable=AsyncMock),
):
# Run the command with skip_source=True
await run_command(str(config_file), skip_source=True)
# Verify prepare was NOT called
prepare_mock.assert_not_called()
async def test_filesystem_source_prepare_by_default(self, tmp_path):
"""Test that FileSystemSource is prepared when using direct file spec."""
from unittest.mock import AsyncMock, patch
from fastmcp.cli.run import run_command
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import (
FileSystemSource,
)
# Create a test server file
test_file = tmp_path / "server.py"
test_file.write_text("""
import fastmcp
mcp = fastmcp.FastMCP("TestServer")
""")
# Mock the prepare method and server run
with (
patch.object(
FileSystemSource, "prepare", new_callable=AsyncMock
) as prepare_mock,
patch("fastmcp.server.server.FastMCP.run_async", new_callable=AsyncMock),
):
# Run with direct file specification
await run_command(str(test_file))
# Verify prepare was called
prepare_mock.assert_called_once()
async def test_filesystem_source_skip_prepare_with_flag(self, tmp_path):
"""Test that FileSystemSource.prepare() is skipped with skip_source flag."""
from unittest.mock import AsyncMock, patch
from fastmcp.cli.run import run_command
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import (
FileSystemSource,
)
# Create a test server file
test_file = tmp_path / "server.py"
test_file.write_text("""
import fastmcp
mcp = fastmcp.FastMCP("TestServer")
""")
# Mock the prepare method and server run
with (
patch.object(
FileSystemSource, "prepare", new_callable=AsyncMock
) as prepare_mock,
patch("fastmcp.server.server.FastMCP.run_async", new_callable=AsyncMock),
):
# Run with direct file specification and skip_source=True
await run_command(str(test_file), skip_source=True)
# Verify prepare was NOT called
prepare_mock.assert_not_called()

Some files were not shown because too many files have changed in this diff Show more