mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
Publish FastMCP 4 (alpha) docs to gofastmcp.com (#4624)
This commit is contained in:
parent
3098f8086b
commit
8cf4506aa9
846 changed files with 86684 additions and 22013 deletions
|
|
@ -31,7 +31,7 @@ merge, not a courtesy.
|
|||
- Maintainer-authored PRs are exempt. A `trusted-contributor` label exempts a contributor up
|
||||
front. Reopening the PR or removing the `missing-issue-link` label applies a sticky
|
||||
`bypass-issue-check`.
|
||||
- Sibling bots have usually already run on the issue: `martian-triage-issue` (investigates +
|
||||
- Sibling bots have usually already run on the issue: `marvin-triage-issue` (investigates +
|
||||
recommends), `marvin-dedupe-issues` / `auto-close-duplicates` (dupes), `auto-close-needs-mre`
|
||||
(missing MRE). Read their comments before re-deriving anything.
|
||||
|
||||
|
|
|
|||
2
.github/actions/run-claude/action.yml
vendored
2
.github/actions/run-claude/action.yml
vendored
|
|
@ -40,7 +40,7 @@ inputs:
|
|||
model:
|
||||
description: "Model to use for Claude"
|
||||
required: false
|
||||
default: "claude-opus-4-6"
|
||||
default: "claude-opus-4-8"
|
||||
|
||||
allowed-bots:
|
||||
description: "Allowed bot usernames, or '*' for all bots"
|
||||
|
|
|
|||
12
.github/actions/run-pytest/action.yml
vendored
12
.github/actions/run-pytest/action.yml
vendored
|
|
@ -19,7 +19,7 @@ runs:
|
|||
MAX_PROCS="2"
|
||||
EXTRA_FLAGS=""
|
||||
elif [ "${{ inputs.test-type }}" == "client_process" ]; then
|
||||
MARKER="client_process"
|
||||
MARKER="client_process or subprocess_heavy"
|
||||
TIMEOUT="5"
|
||||
MAX_PROCS="0"
|
||||
EXTRA_FLAGS="-x"
|
||||
|
|
@ -29,14 +29,20 @@ runs:
|
|||
MAX_PROCS="0"
|
||||
EXTRA_FLAGS="-x"
|
||||
else
|
||||
MARKER="not integration and not client_process and not conformance"
|
||||
MARKER="not integration and not client_process and not subprocess_heavy and not conformance"
|
||||
TIMEOUT="5"
|
||||
MAX_PROCS="4"
|
||||
EXTRA_FLAGS=""
|
||||
fi
|
||||
|
||||
# Windows previously ran serially: parallel workers crashed intermittently
|
||||
# when many tests spawned stdio subprocesses (#2715, reverted in #2726).
|
||||
# Most of those tests now run in-memory, but tests that spawn a fresh
|
||||
# interpreter importing all of FastMCP still crash xdist workers on the
|
||||
# 2-core Windows runners. They carry the subprocess_heavy marker and run
|
||||
# in the serial client_process step instead.
|
||||
PARALLEL_FLAGS=""
|
||||
if [ "$MAX_PROCS" != "0" ] && [ "${{ runner.os }}" != "Windows" ]; then
|
||||
if [ "$MAX_PROCS" != "0" ]; then
|
||||
PARALLEL_FLAGS="--numprocesses auto --maxprocesses $MAX_PROCS --dist worksteal"
|
||||
fi
|
||||
|
||||
|
|
|
|||
80
.github/scripts/triage-label.sh
vendored
Executable file
80
.github/scripts/triage-label.sh
vendored
Executable file
|
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env bash
|
||||
# Locked-down label helper for the Marvin triage workflow.
|
||||
#
|
||||
# Marvin runs on untrusted issue/PR bodies from non-write users, so it must
|
||||
# NOT be handed raw `gh api` (that would expose every endpoint the app token
|
||||
# can reach). This helper is the ONLY GitHub write it is allowed to perform:
|
||||
# it adds or removes repository labels on the one issue/PR being triaged.
|
||||
#
|
||||
# The target repo and number come from the environment set by the workflow —
|
||||
# never from the model — and the operation is fixed to the additive labels
|
||||
# endpoint (POST/DELETE /repos/{repo}/issues/{n}/labels), which works for both
|
||||
# issues and PRs and cannot clobber labels applied by other workflows.
|
||||
set -euo pipefail
|
||||
|
||||
repo="${TRIAGE_REPO:?TRIAGE_REPO not set}"
|
||||
number="${TRIAGE_NUMBER:?TRIAGE_NUMBER not set}"
|
||||
|
||||
if [[ ! "$number" =~ ^[0-9]+$ ]]; then
|
||||
echo "TRIAGE_NUMBER must be numeric, got: $number" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
op="${1:-}"
|
||||
shift || true
|
||||
case "$op" in
|
||||
add) method=POST ;;
|
||||
remove) method=DELETE ;;
|
||||
*)
|
||||
echo "usage: triage-label.sh <add|remove> <label>..." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$#" -eq 0 ]]; then
|
||||
echo "no labels given" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Reject anything that isn't a plausible label name. Notably blocks '/' so a
|
||||
# crafted value can't turn the DELETE path into a different endpoint.
|
||||
label_re="^[A-Za-z0-9 ._'-]+$"
|
||||
for label in "$@"; do
|
||||
if [[ ! "$label" =~ $label_re ]]; then
|
||||
echo "refusing suspicious label name: $label" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Never let triage add or remove the Require Issue Link control labels. Those
|
||||
# govern PR enforcement (bypass-issue-check / trusted-contributor are sticky
|
||||
# exemptions, "prs welcome" waives the assignment requirement) and reopening
|
||||
# (missing-issue-link is how closed PRs are found), so a prompt-injected triage
|
||||
# run must not be able to grant an exemption or break recovery. Enforced here —
|
||||
# in code — not merely in the prompt.
|
||||
#
|
||||
# Exact match against array entries, not a substring scan of a joined string:
|
||||
# label names may contain spaces ("prs welcome"), which in a space-delimited
|
||||
# string would also make bare "prs" and "welcome" match.
|
||||
protected=(missing-issue-link bypass-issue-check trusted-contributor "prs welcome")
|
||||
for label in "$@"; do
|
||||
lower="${label,,}"
|
||||
for p in "${protected[@]}"; do
|
||||
if [[ "$lower" == "$p" ]]; then
|
||||
echo "refusing to touch protected control label: $label" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [[ "$method" == POST ]]; then
|
||||
args=()
|
||||
for label in "$@"; do
|
||||
args+=(-f "labels[]=$label")
|
||||
done
|
||||
gh api --method POST "/repos/${repo}/issues/${number}/labels" "${args[@]}"
|
||||
else
|
||||
for label in "$@"; do
|
||||
gh api --method DELETE "/repos/${repo}/issues/${number}/labels/${label}"
|
||||
done
|
||||
fi
|
||||
4
.github/workflows/marvin-dedupe-issues.yml
vendored
4
.github/workflows/marvin-dedupe-issues.yml
vendored
|
|
@ -100,10 +100,10 @@ jobs:
|
|||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }}
|
||||
allowed_non_write_users: "*"
|
||||
claude_args: |
|
||||
--allowedTools Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh api:*),Bash(gh issue comment:*),Task
|
||||
--allowedTools "Bash(gh issue view:*)","Bash(gh search:*)","Bash(gh issue list:*)","Bash(gh api:*)","Bash(gh issue comment:*)",Task
|
||||
settings: |
|
||||
{
|
||||
"model": "claude-sonnet-4-6",
|
||||
"model": "claude-sonnet-5",
|
||||
"env": {
|
||||
"GH_TOKEN": "${{ steps.marvin-token.outputs.token }}"
|
||||
}
|
||||
|
|
|
|||
91
.github/workflows/marvin-label-triage.yml
vendored
91
.github/workflows/marvin-label-triage.yml
vendored
|
|
@ -49,13 +49,16 @@ jobs:
|
|||
PROMPT<<PROMPT_END
|
||||
You're an issue triage assistant for FastMCP, a Python framework for building Model Context Protocol servers and clients. Your task is to analyze issues/PRs and apply appropriate labels.
|
||||
|
||||
IMPORTANT: Your primary action should be to apply labels using mcp__github__update_issue. DO NOT post comments EXCEPT when applying the too-long label (see below).
|
||||
IMPORTANT: Your primary action should be to apply labels using the locked-down helper `.github/scripts/triage-label.sh`. DO NOT post comments EXCEPT when applying the too-long label (see below).
|
||||
|
||||
CRITICAL — LABEL MECHANICS:
|
||||
- `mcp__github__update_issue` REPLACES all labels on the issue — it does not add to them.
|
||||
- Before applying labels, read the issue's current labels with `mcp__github__get_issue`.
|
||||
- Always include any existing labels you want to keep alongside the new ones.
|
||||
- Apply labels ONLY through the helper, which adds or removes repository labels on THIS issue/PR. It already knows the target repo and number (from the workflow environment) — you never pass them:
|
||||
add: `bash .github/scripts/triage-label.sh add "label1" "label2"`
|
||||
remove: `bash .github/scripts/triage-label.sh remove "label1"`
|
||||
- The helper uses the additive REST labels endpoint, so it works for both issues and PRs and never clobbers labels applied by other workflows — notably the Require Issue Link workflow's `missing-issue-link` control label, which must survive or an auto-closed PR won't reopen when its author is assigned.
|
||||
- The helper is your ONLY GitHub write access. Do NOT use raw `gh api`, `gh issue edit`, `gh pr edit`, or any other mutation — they are not available to you.
|
||||
- Only apply labels that exist in the repository (from `gh label list` in step 1). Never invent labels.
|
||||
- Use `remove` only to correct a label you believe is wrong, and never remove the control labels `missing-issue-link`, `bypass-issue-check`, or `trusted-contributor`.
|
||||
|
||||
Issue/PR Information:
|
||||
- REPO: ${{ github.repository }}
|
||||
|
|
@ -131,7 +134,7 @@ jobs:
|
|||
- DON'T MERGE: Only if PR author explicitly states it's not ready
|
||||
|
||||
4. Apply selected labels:
|
||||
Use mcp__github__update_issue to apply your selected labels
|
||||
Add them with `bash .github/scripts/triage-label.sh add "label1" "label2"`.
|
||||
DO NOT post any comments unless applying too-long (see above)
|
||||
PROMPT_END
|
||||
EOF
|
||||
|
|
@ -140,6 +143,7 @@ jobs:
|
|||
run: rm -rf ~/.claude/.locks ~/.local/state/claude/locks || true
|
||||
|
||||
- name: Run Marvin for Issue Triage
|
||||
id: marvin
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
github_token: ${{ steps.marvin-token.outputs.token }}
|
||||
|
|
@ -149,11 +153,82 @@ jobs:
|
|||
allowed_non_write_users: "*"
|
||||
allowed_bots: "marvin-context-protocol"
|
||||
claude_args: |
|
||||
--allowedTools Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__add_issue_comment,mcp__github__get_pull_request_files
|
||||
--allowedTools "Bash(gh label list:*)","Bash(bash .github/scripts/triage-label.sh:*)",mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__add_issue_comment,mcp__github__get_pull_request,mcp__github__get_pull_request_files
|
||||
settings: |
|
||||
{
|
||||
"model": "claude-sonnet-4-6",
|
||||
"model": "claude-sonnet-5",
|
||||
"env": {
|
||||
"GH_TOKEN": "${{ steps.marvin-token.outputs.token }}"
|
||||
"GH_TOKEN": "${{ steps.marvin-token.outputs.token }}",
|
||||
"TRIAGE_REPO": "${{ github.repository }}",
|
||||
"TRIAGE_NUMBER": "${{ github.event.issue.number || github.event.pull_request.number || inputs.issue_number }}"
|
||||
}
|
||||
}
|
||||
|
||||
# Triage is fire-and-forget: nobody watches a green run, so a broken
|
||||
# allowlist has to fail the job or it goes unnoticed indefinitely — a
|
||||
# mangled pattern silently produced zero labels across a dozen PRs
|
||||
# because the run still reported success.
|
||||
#
|
||||
# Only denials of commands we MEANT to grant indicate that breakage. An
|
||||
# agent reaching for something never on the allowlist (falling back to
|
||||
# `gh issue view` when the API is down, say) is behaving normally, and
|
||||
# failing on that would cry wolf during every GitHub incident.
|
||||
- name: Fail if an allowlisted tool was denied
|
||||
if: always() && steps.marvin.conclusion != 'skipped'
|
||||
env:
|
||||
EXECUTION_FILE: ${{ steps.marvin.outputs.execution_file }}
|
||||
run: |
|
||||
file="${EXECUTION_FILE:-}"
|
||||
if [[ -z "$file" || ! -s "$file" ]]; then
|
||||
file="${RUNNER_TEMP}/claude-execution-output.json"
|
||||
fi
|
||||
# A missing or empty log means we cannot tell a clean run from a
|
||||
# blocked one, which is the exact failure this step exists to catch.
|
||||
if [[ ! -s "$file" ]]; then
|
||||
echo "::error::No Marvin execution log found; cannot verify tool permissions."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The persisted log carries a `permission_denials` array on each
|
||||
# `type: result` entry; the `permission_denials_count` scalar only
|
||||
# appears in the action's condensed stdout summary, never on disk.
|
||||
# Anchor to result entries rather than recursing with `..`, which
|
||||
# descends into each denial's `tool_input` and double-counts any
|
||||
# denied command that happens to mention the field name.
|
||||
if ! summary=$(jq -sr '
|
||||
[ .[] | if type == "array" then .[] else . end ]
|
||||
| map(select(type == "object" and .type == "result"))
|
||||
| map(.permission_denials // []) | flatten
|
||||
| map(.tool_input.command // "")
|
||||
| { total: length,
|
||||
granted: map(select(
|
||||
startswith("gh label list")
|
||||
or startswith("bash .github/scripts/triage-label.sh")
|
||||
))
|
||||
}
|
||||
| "\(.total)\t\(.granted | length)\t\(.granted | join(" | "))"
|
||||
' "$file"); then
|
||||
echo "::error::Could not parse Marvin execution log ($file)."
|
||||
exit 1
|
||||
fi
|
||||
IFS=$'\t' read -r total granted commands <<<"$summary"
|
||||
echo "Denied tool calls: $total (of which allowlisted: $granted)"
|
||||
|
||||
if [[ "$granted" -gt 0 ]]; then
|
||||
echo "::error::Marvin was denied $granted call(s) to tools this workflow grants, so it could not apply labels: ${commands}. The --allowedTools value is not reaching the permission matcher intact — claude_args is lexed with shell-quote, so any Bash(...) pattern containing a space must be quoted or it is split into fragments."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$total" -gt 0 ]]; then
|
||||
echo "::notice::Marvin was denied $total call(s), none of them to tools this workflow grants. That is expected when it probes for a tool we deliberately withhold; the allowlist is intact."
|
||||
fi
|
||||
|
||||
- name: Upload Marvin execution log
|
||||
if: always() && steps.marvin.conclusion != 'skipped'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marvin-triage-execution-log
|
||||
path: |
|
||||
${{ steps.marvin.outputs.execution_file }}
|
||||
${{ runner.temp }}/claude-execution-output.json
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
martian-test-failure:
|
||||
marvin-test-failure:
|
||||
# Only run if the test workflow failed
|
||||
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -193,5 +193,5 @@ jobs:
|
|||
|
||||
prompt: ${{ steps.analysis-prompt.outputs.PROMPT }}
|
||||
claude_args: |
|
||||
--allowed-tools mcp__repository-summary,mcp__code-search,mcp__github-research,WebSearch,WebFetch,Bash(make:*,git:*)
|
||||
--allowed-tools mcp__repository-summary,mcp__code-search,mcp__github-research,WebSearch,WebFetch,"Bash(make:*)","Bash(git:*)"
|
||||
--mcp-config /tmp/mcp-config/mcp-servers.json
|
||||
87
.github/workflows/publish-fastmcp-tasks.yml
vendored
Normal file
87
.github/workflows/publish-fastmcp-tasks.yml
vendored
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
name: Publish fastmcp-tasks to PyPI
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Publish fastmcp-slim to PyPI"]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
pypi-publish:
|
||||
name: Upload fastmcp-tasks to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'release')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Build fastmcp-tasks
|
||||
run: uv build --package fastmcp-tasks
|
||||
|
||||
- name: Verify matching fastmcp-slim is published
|
||||
run: |
|
||||
SLIM_VERSION=$(python - <<'PY'
|
||||
import email.parser
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
wheel = next(Path("dist").glob("fastmcp_tasks-*.whl"))
|
||||
metadata_name = next(
|
||||
name for name in zipfile.ZipFile(wheel).namelist()
|
||||
if name.endswith(".dist-info/METADATA")
|
||||
)
|
||||
metadata = email.parser.Parser().parsestr(
|
||||
zipfile.ZipFile(wheel).read(metadata_name).decode()
|
||||
)
|
||||
for value in metadata.get_all("Requires-Dist", []):
|
||||
requirement, _, marker = value.partition(";")
|
||||
if marker.strip():
|
||||
continue
|
||||
match = re.fullmatch(
|
||||
r"fastmcp-slim(?:\[[^\]]+\])?==([^;\s]+)",
|
||||
requirement.strip(),
|
||||
)
|
||||
if match:
|
||||
print(match.group(1))
|
||||
break
|
||||
else:
|
||||
raise RuntimeError("Could not find the base fastmcp-slim dependency")
|
||||
PY
|
||||
)
|
||||
|
||||
for attempt in {1..12}; do
|
||||
if python - "$SLIM_VERSION" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
version = sys.argv[1]
|
||||
url = f"https://pypi.org/pypi/fastmcp-slim/{version}/json"
|
||||
with urllib.request.urlopen(url, timeout=30) as response:
|
||||
json.load(response)
|
||||
PY
|
||||
then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "fastmcp-slim ${SLIM_VERSION} is not available on PyPI yet; retrying (${attempt}/12)."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo "fastmcp-slim ${SLIM_VERSION} is not available on PyPI; refusing to publish fastmcp-tasks." >&2
|
||||
exit 1
|
||||
|
||||
- name: Publish fastmcp-tasks to PyPI
|
||||
run: uv publish -v dist/fastmcp_tasks-*.tar.gz dist/fastmcp_tasks-*.whl
|
||||
66
.github/workflows/publish-fastmcp.yml
vendored
66
.github/workflows/publish-fastmcp.yml
vendored
|
|
@ -115,6 +115,58 @@ jobs:
|
|||
echo "fastmcp-slim ${SLIM_VERSION} is not available on PyPI; refusing to publish fastmcp." >&2
|
||||
exit 1
|
||||
|
||||
- name: Verify matching fastmcp-tasks is published
|
||||
run: |
|
||||
TASKS_VERSION=$(python - <<'PY'
|
||||
import email.parser
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
wheel = next(Path("dist").glob("fastmcp-*.whl"))
|
||||
metadata_name = next(
|
||||
name for name in zipfile.ZipFile(wheel).namelist()
|
||||
if name.endswith(".dist-info/METADATA")
|
||||
)
|
||||
metadata = email.parser.Parser().parsestr(
|
||||
zipfile.ZipFile(wheel).read(metadata_name).decode()
|
||||
)
|
||||
# fastmcp-tasks is pinned via the optional `tasks` extra, so its
|
||||
# Requires-Dist entry carries an `extra == "tasks"` marker — unlike the
|
||||
# base slim dependency, do not skip marked entries here.
|
||||
for value in metadata.get_all("Requires-Dist", []):
|
||||
requirement, _, _marker = value.partition(";")
|
||||
match = re.fullmatch(r"fastmcp-tasks==([^;\s]+)", requirement.strip())
|
||||
if match:
|
||||
print(match.group(1))
|
||||
break
|
||||
else:
|
||||
raise RuntimeError("Could not find the fastmcp-tasks extra dependency")
|
||||
PY
|
||||
)
|
||||
|
||||
for attempt in {1..12}; do
|
||||
if python - "$TASKS_VERSION" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
version = sys.argv[1]
|
||||
url = f"https://pypi.org/pypi/fastmcp-tasks/{version}/json"
|
||||
with urllib.request.urlopen(url, timeout=30) as response:
|
||||
json.load(response)
|
||||
PY
|
||||
then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "fastmcp-tasks ${TASKS_VERSION} is not available on PyPI yet; retrying (${attempt}/12)."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo "fastmcp-tasks ${TASKS_VERSION} is not available on PyPI; refusing to publish fastmcp (the [tasks] extra would be uninstallable)." >&2
|
||||
exit 1
|
||||
|
||||
- name: Publish fastmcp to PyPI
|
||||
run: uv publish -v dist/fastmcp-*.tar.gz dist/fastmcp-*.whl
|
||||
|
||||
|
|
@ -133,5 +185,19 @@ jobs:
|
|||
fetch-depth: 0
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
|
||||
- name: Check release line
|
||||
id: release_line
|
||||
env:
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
run: |
|
||||
git fetch origin "${DEFAULT_BRANCH}:refs/remotes/origin/${DEFAULT_BRANCH}"
|
||||
if git merge-base --is-ancestor HEAD "refs/remotes/origin/${DEFAULT_BRANCH}"; then
|
||||
echo "update_published_docs=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "update_published_docs=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Release commit is not on ${DEFAULT_BRANCH}; skipping published-docs update."
|
||||
fi
|
||||
|
||||
- name: Point published-docs at published release
|
||||
if: steps.release_line.outputs.update_published_docs == 'true'
|
||||
run: git push --force origin "HEAD:published-docs"
|
||||
|
|
|
|||
50
.github/workflows/require-issue-link.yml
vendored
50
.github/workflows/require-issue-link.yml
vendored
|
|
@ -1,5 +1,8 @@
|
|||
# Require external PRs to reference an issue with an auto-close keyword
|
||||
# (e.g. "Fixes #123") AND have the PR author assigned to that issue.
|
||||
# (e.g. "Fixes #123") AND have the PR author assigned to that issue —
|
||||
# unless the referenced issue is labeled "prs welcome", which waives the
|
||||
# assignment requirement for everyone (the link itself is still required,
|
||||
# since that's how the check finds the issue to read the label from).
|
||||
# Otherwise the PR is labeled "missing-issue-link", commented on, and
|
||||
# closed. CONTRIBUTING.md requires external contributors to be assigned to
|
||||
# an issue before opening a PR; this enforces that.
|
||||
|
|
@ -96,6 +99,8 @@ jobs:
|
|||
const enforce = process.env.ENFORCE_ISSUE_LINK === 'true';
|
||||
const LABEL = 'missing-issue-link';
|
||||
const MARKER = '<!-- require-issue-link -->';
|
||||
// Issue-level label that waives the assignment requirement.
|
||||
const OPEN_LABEL = 'prs welcome';
|
||||
|
||||
// Dry-run guard: every mutating call goes through this so that
|
||||
// ENFORCE_ISSUE_LINK=false means strictly read-only.
|
||||
|
|
@ -300,6 +305,13 @@ jobs:
|
|||
// CONTRIBUTING.md requires external contributors to be assigned
|
||||
// before opening a PR (so maintainers can deconflict / steer
|
||||
// approach first).
|
||||
//
|
||||
// Exception: an issue labeled OPEN_LABEL waives that requirement
|
||||
// for everyone. It's how maintainers advertise "the reporter
|
||||
// isn't implementing this, we'd take a PR from anyone" without
|
||||
// having to assign a specific person up front. Unlike the
|
||||
// PR-level `trusted-contributor` / `bypass-issue-check` escapes,
|
||||
// this one lives on the *issue* and is set ahead of time.
|
||||
const MAX_ISSUES = 5;
|
||||
const allNumbers = [...new Set(matches.map(m => parseInt(m[1], 10)))];
|
||||
const numbers = allNumbers.slice(0, MAX_ISSUES);
|
||||
|
|
@ -326,6 +338,19 @@ jobs:
|
|||
throw new Error(`Cannot fetch issue #${num} (HTTP ${e.status ?? 'unknown'}): ${e.message}`);
|
||||
}
|
||||
sawRealIssue = true;
|
||||
|
||||
// GitHub returns labels as objects here, but the REST schema
|
||||
// permits bare strings — normalize both rather than assume.
|
||||
const labelNames = (issue.labels || [])
|
||||
.map(l => (typeof l === 'string' ? l : l && l.name))
|
||||
.filter(Boolean)
|
||||
.map(n => n.toLowerCase());
|
||||
if (labelNames.includes(OPEN_LABEL)) {
|
||||
console.log(`#${num} is labeled "${OPEN_LABEL}" — assignment not required`);
|
||||
assignedToAny = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const assignees = (issue.assignees || []).map(a => a.login.toLowerCase());
|
||||
if (assignees.includes(prAuthor)) {
|
||||
console.log(`PR author ${pr.user.login} is assigned to #${num}`);
|
||||
|
|
@ -354,29 +379,30 @@ jobs:
|
|||
async function enforceFailure(kind) {
|
||||
await addLabel();
|
||||
|
||||
const intro = kind === 'no-link'
|
||||
? '**This PR has been automatically closed** because its description does not reference a tracked issue.'
|
||||
: '**This PR has been automatically closed** because you are not assigned to the issue it references.';
|
||||
const reason = kind === 'no-link'
|
||||
? "it doesn't reference a tracked issue assigned to you"
|
||||
: "you aren't assigned to the issue it references";
|
||||
const steps = kind === 'no-link'
|
||||
? [
|
||||
`1. Find or [open an issue](https://github.com/${owner}/${repo}/issues/new/choose) describing the change.`,
|
||||
'2. Comment on the issue to ask a maintainer to assign it to you.',
|
||||
'3. Add `Fixes #<issue>`, `Closes #<issue>`, or `Resolves #<issue>` to the PR description.',
|
||||
'4. Once you are assigned and the link is present, the PR reopens automatically.',
|
||||
`1. Find or [open an issue](https://github.com/${owner}/${repo}/issues/new/choose) describing the change — if you open it, you have first claim on it.`,
|
||||
"2. Add `Fixes #<issue>`, `Closes #<issue>`, or `Resolves #<issue>` to **this** PR's description — edit it in place, don't open a new PR.",
|
||||
]
|
||||
: [
|
||||
'1. Comment on the linked issue to ask a maintainer to assign it to you.',
|
||||
'2. Once a maintainer assigns you, the PR reopens automatically.',
|
||||
"1. If you opened the linked issue, a maintainer will assign you when they pick it up and this PR reopens automatically. If someone else opened it, the PR reopens only if a maintainer chooses to assign it to you — please don't comment to ask.",
|
||||
];
|
||||
|
||||
const commentBody = [
|
||||
MARKER,
|
||||
intro,
|
||||
"**Don't open a new pull request — this one reopens on its own.** It's closed for " +
|
||||
`now because ${reason}, but the moment that's fixed it reopens automatically. Keep this ` +
|
||||
'PR and edit it; opening a fresh duplicate just starts you over and creates more to triage.',
|
||||
'',
|
||||
`Per [CONTRIBUTING.md](https://github.com/${owner}/${repo}/blob/main/CONTRIBUTING.md), an external PR must reference an issue that is assigned to its author. To proceed:`,
|
||||
`Per [CONTRIBUTING.md](https://github.com/${owner}/${repo}/blob/main/CONTRIBUTING.md), an external PR must reference an issue that's assigned to its author. To get there:`,
|
||||
'',
|
||||
...steps,
|
||||
'',
|
||||
"Once you're assigned and the link is present, this PR reopens automatically — no further action needed.",
|
||||
'',
|
||||
`*Maintainers: reopen this PR or remove the \`${LABEL}\` label to bypass this check.*`,
|
||||
].join('\n');
|
||||
|
||||
|
|
|
|||
1
.github/workflows/run-static.yml
vendored
1
.github/workflows/run-static.yml
vendored
|
|
@ -10,6 +10,7 @@ on:
|
|||
- "fastmcp_slim/**"
|
||||
- "fastmcp_remote/**"
|
||||
- "tests/**"
|
||||
- "examples/**"
|
||||
- "pyproject.toml"
|
||||
- "uv.lock"
|
||||
- ".github/workflows/**"
|
||||
|
|
|
|||
10
.github/workflows/run-tests.yml
vendored
10
.github/workflows/run-tests.yml
vendored
|
|
@ -48,7 +48,7 @@ jobs:
|
|||
- name: Run unit tests
|
||||
uses: ./.github/actions/run-pytest
|
||||
|
||||
- name: Run client process tests
|
||||
- name: Run serial subprocess tests
|
||||
uses: ./.github/actions/run-pytest
|
||||
with:
|
||||
test-type: client_process
|
||||
|
|
@ -69,7 +69,7 @@ jobs:
|
|||
- name: Run unit tests
|
||||
uses: ./.github/actions/run-pytest
|
||||
|
||||
- name: Run client process tests
|
||||
- name: Run serial subprocess tests
|
||||
uses: ./.github/actions/run-pytest
|
||||
with:
|
||||
test-type: client_process
|
||||
|
|
@ -88,7 +88,7 @@ jobs:
|
|||
resolution: locked
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
|
|
@ -222,7 +222,7 @@ jobs:
|
|||
run: |
|
||||
uv venv /tmp/fastmcp-full-smoke
|
||||
FULL_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp-*.whl)
|
||||
uv pip install --python /tmp/fastmcp-full-smoke/bin/python --find-links /tmp/fastmcp-dist "$FULL_WHEEL"
|
||||
uv pip install --python /tmp/fastmcp-full-smoke/bin/python --prerelease=allow --find-links /tmp/fastmcp-dist "$FULL_WHEEL"
|
||||
/tmp/fastmcp-full-smoke/bin/python - <<'PY'
|
||||
from importlib.metadata import entry_points
|
||||
from importlib.metadata import requires
|
||||
|
|
@ -250,7 +250,7 @@ jobs:
|
|||
run: |
|
||||
uv venv /tmp/fastmcp-remote-smoke
|
||||
REMOTE_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp_remote-*.whl)
|
||||
uv pip install --python /tmp/fastmcp-remote-smoke/bin/python --find-links /tmp/fastmcp-dist "$REMOTE_WHEEL"
|
||||
uv pip install --python /tmp/fastmcp-remote-smoke/bin/python --prerelease=allow --find-links /tmp/fastmcp-dist "$REMOTE_WHEEL"
|
||||
/tmp/fastmcp-remote-smoke/bin/python - <<'PY'
|
||||
from importlib.metadata import entry_points
|
||||
from importlib.metadata import requires
|
||||
|
|
|
|||
2
.github/workflows/run-upgrade-checks.yml
vendored
2
.github/workflows/run-upgrade-checks.yml
vendored
|
|
@ -67,7 +67,7 @@ jobs:
|
|||
- name: Run unit tests
|
||||
uses: ./.github/actions/run-pytest
|
||||
|
||||
- name: Run client process tests
|
||||
- name: Run serial subprocess tests
|
||||
uses: ./.github/actions/run-pytest
|
||||
with:
|
||||
test-type: client_process
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ repos:
|
|||
entry: uv run --isolated ty check
|
||||
language: system
|
||||
types: [python]
|
||||
files: ^fastmcp_slim/|^tests/
|
||||
files: ^fastmcp_slim/|^tests/|^examples/
|
||||
pass_filenames: false
|
||||
require_serial: true
|
||||
|
||||
|
|
|
|||
10
CLAUDE.md
10
CLAUDE.md
|
|
@ -84,12 +84,12 @@ Only cut releases when the maintainer explicitly asks. Tags follow `v<version>`
|
|||
Write the maintainer-approved handwritten notes to a temporary file, then create the release. `--generate-notes` appends the auto-generated changelog after the handwritten content.
|
||||
|
||||
```bash
|
||||
gh release create v3.2.0 --target main --title "v3.2.0: Theme Here" --generate-notes --notes-start-tag v3.1.1 --notes-file /tmp/release-notes.md
|
||||
gh release create v4.0.0 --target main --title "v4.0.0: Theme Here" --generate-notes --notes-start-tag v3.4.4 --notes-file /tmp/release-notes.md
|
||||
```
|
||||
|
||||
**Always pass `--notes-start-tag <last-stable-tag>`.** Without it, `--generate-notes` picks the most recent prior tag as the changelog start point — and if a prerelease exists (e.g. `v3.4.0b1`), it starts from *that*, silently truncating the PR list to only the commits since the beta. Pin it to the last stable release (e.g. `v3.3.1` when cutting `v3.4.0`). Verify after: the compare link at the bottom of the generated notes should read `v<last-stable>...v<new>`.
|
||||
|
||||
Most releases target `main`, but maintenance or backport releases may target a different branch (e.g., `release/2.x`). Confirm the target with the maintainer if there's any ambiguity.
|
||||
Use the branch that owns the release line as the target: current-major releases target `main`, 3.x maintenance releases target `release/3.x`, and 2.x maintenance releases target `release/2.x`. Confirm the target with the maintainer if there's any ambiguity. For example, cut a 3.4.4 maintenance release with `--target release/3.x`, not `main`.
|
||||
|
||||
The handwritten notes are prepended above the auto-generated changelog and are the part that matters. Do not include a title in the notes body — the release title (`v{version}: {pun}`) already serves as the heading. Work with the maintainer to draft the notes — propose a draft, get feedback, iterate. Do not publish without the maintainer's sign-off.
|
||||
|
||||
|
|
@ -105,16 +105,18 @@ gh api -X POST repos/PrefectHQ/fastmcp/releases/generate-notes \
|
|||
--jq '.body'
|
||||
```
|
||||
|
||||
Set `target_commitish` to the same branch that will receive the release tag. For maintenance releases, use the maintenance branch (for example, `release/3.x`) so the preview matches the release notes GitHub will generate.
|
||||
|
||||
**Point releases** (3.0, 3.1, 3.2) get narrative prose: open with the theme of the release, then walk through headline features conceptually — what they enable, why they matter, how they fit together. Write it the way a blog post reads, not a changelog. Multiple paragraphs, code examples where they clarify.
|
||||
|
||||
**Patch releases** (3.1.1, 3.0.2) get 1-2 sentences explaining what broke and what the fix does. Keep it minimal — the auto-generated changelog has the details.
|
||||
|
||||
**Merge the docs changelog PR *before* cutting the release, not after.** The post-publish `update-published-docs` job force-pushes the `published-docs` branch (which gofastmcp.com serves) to the *released commit* — so the changelog entry only reaches the live site if it's already in the commit being tagged. Land the docs PR on `main` first, then cut the release from `main`. If you tag first and merge docs after, this release's changelog won't appear on the live site until the *next* release force-pushes `published-docs` forward. Two hand-maintained files mirror the GitHub release and must get a new entry for every version, newest at the top (these are `.mdx` and are not covered by the prek Prettier hook, which only runs on `yaml`/`json5` — match the existing entries' style by hand):
|
||||
**Merge the docs changelog PR *before* cutting the release, not after.** The post-publish `update-published-docs` job force-pushes the `published-docs` branch (which gofastmcp.com serves) to the released commit for stable releases on the default branch, so the changelog entry only reaches the live site if it's already in the commit being tagged. Land the docs PR on the release target branch first, then cut the release from that branch. If you tag first and merge docs after, this release's changelog won't appear on the live site until the next default-branch stable release force-pushes `published-docs` forward. Maintenance releases from `release/3.x` or `release/2.x` publish packages and GitHub notes without repointing `published-docs`; add their changelog entries on the maintenance branch, slotted into the matching major-version section. Two hand-maintained files mirror the GitHub release and must get a new entry for every version, newest at the top (these are `.mdx` and are not covered by the prek Prettier hook, which only runs on `yaml`/`json5` — match the existing entries' style by hand):
|
||||
|
||||
- `docs/changelog.mdx` is the full mirror. Add an `<Update label="v<version>" description="YYYY-MM-DD">` block with: a bold linked title (`**[v<version>: <pun>](<release-url>)**`), a condensed 1-paragraph intro (one sentence for patches), the full categorized PR list reformatted from the `--generate-notes` output (`* <title> by [@user](https://github.com/user) in [#NNNN](<pull-url>)`), a `## New Contributors` list (plain `@user`, linked PR), and a `**Full Changelog**: [vA...vB](<compare-url>)` line.
|
||||
- `docs/updates.mdx` is the skimmable card feed. Add an `<Update label="FastMCP <version>" description="Month DD, YYYY" tags={["Releases"]}>` wrapping a `<Card>` that links to the GitHub release, with a 1-2 sentence summary and (for point releases) a handful of emoji-bulleted highlights.
|
||||
|
||||
Because the docs land *before* the tag exists, derive the entry from the maintainer-approved handwritten notes (intro/summary) and the `--generate-notes` API *preview* (the PR-list body — see the generate-notes API call above, which returns the exact changelog without cutting anything). Scripting the link reformatting is reliable for long PR lists. The release-URL, tag, and compare links follow the known pattern (`/releases/tag/v<version>`, `compare/v<last-stable>...v<version>`) and will 404 only during the short window between merging the docs PR and cutting the release minutes later — they resolve before `published-docs` ever deploys, since that happens after the full publish chain. For this reason, create and merge the docs PR *immediately* before cutting the release — treat the two as one tight back-to-back sequence, not independent steps — so the links are valid by the time the release publishes rather than dangling for any longer than necessary. Maintenance/backport releases (e.g. `v2.14.7`) get an entry in the same two files, slotted into the 2.x section.
|
||||
Because the docs land *before* the tag exists, derive the entry from the maintainer-approved handwritten notes (intro/summary) and the `--generate-notes` API *preview* (the PR-list body — see the generate-notes API call above, which returns the exact changelog without cutting anything). Scripting the link reformatting is reliable for long PR lists. The release-URL, tag, and compare links follow the known pattern (`/releases/tag/v<version>`, `compare/v<last-stable>...v<version>`) and will 404 only during the short window between merging the docs PR and cutting the release minutes later — they resolve before the release workflow completes. For this reason, create and merge the docs PR *immediately* before cutting the release — treat the two as one tight back-to-back sequence, not independent steps — so the links are valid by the time the release publishes rather than dangling for any longer than necessary.
|
||||
|
||||
### Commit Messages and Agent Attribution
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
FastMCP is an actively maintained, high-traffic project. We welcome contributions — but the most impactful way to contribute might not be what you expect.
|
||||
|
||||
Participation is governed by our [Code of Conduct](CODE_OF_CONDUCT.md), and contributions are licensed under [Apache 2.0](LICENSE).
|
||||
|
||||
## The best contribution is a great issue
|
||||
|
||||
FastMCP is an opinionated framework, and its maintainers use AI-assisted tooling that is deeply tuned to those opinions — the design philosophy, the API patterns, the way the framework is meant to evolve. A well-written issue with a clear problem description is often more valuable than a pull request, because it lets maintainers produce a solution that isn't just correct, but consistent with how the framework wants to work. That matters more than speed, though it's faster too.
|
||||
|
|
@ -18,9 +20,17 @@ That's it. No need to diagnose root causes, propose API designs, or suggest impl
|
|||
|
||||
We encourage you to use LLMs to help identify bugs, write MREs, and prepare contributions. But if you do, your LLM must take into account the conventions and contributing guidelines of this repo — including how we want issues formatted and when it's appropriate to open a PR. Generic LLM output that ignores these guidelines tells us the contribution wasn't made thoughtfully, and we will close it. A good AI-assisted contribution is indistinguishable from a good human one. A bad one is obvious.
|
||||
|
||||
If you're driving an agent: do **not** have it post comments asking to be assigned to an issue or announcing that it intends to work on one. Those comments are ignored. If the agent intends to contribute, open a PR instead — it will be gated on assignment (see below). Comment on an issue only to propose a genuinely novel, differentiated solution, never to claim a task that's already described.
|
||||
|
||||
## When to open a pull request
|
||||
|
||||
An open issue is not an invitation to submit a PR. Issues track problems; whether and how to solve them is a separate decision. If you want to work on something, propose your approach in the issue first and ask a maintainer to assign it to you — especially for anything beyond a trivial fix. External PRs that reference an issue not assigned to their author are closed automatically (see [PR guidelines](#pr-guidelines)).
|
||||
An open issue is not an invitation to submit a PR, and it is not a queue you join by commenting. Issues track problems; who implements them and how is a separate decision maintainers make, and whoever opened the issue has first claim on it.
|
||||
|
||||
**Don't post drive-by comments claiming an issue** — "can I work on this?", "please assign me", "I'll take this." They don't affect who gets assigned, they're the most common form of noise we get, and automated versions are ignored. Whoever opens the issue has first claim on it; if that's you, a maintainer will assign you. If you want to implement something someone else reported, just open a PR — you don't need permission to try, and competing PRs are fine — but it's reviewed only if a maintainer assigns you to the issue, which usually won't happen if the reporter intends to handle it. The one comment worth posting is a genuinely different approach worth discussing; a substantive design proposal is welcome, a bare claim on the task is not.
|
||||
|
||||
**Issues labeled `prs welcome` skip the assignment gate.** When we apply that label, we're saying the reporter isn't implementing it and we'd take a PR from anyone. Open one directly — no assignment needed, and it won't be auto-closed. Still reference the issue (`Fixes #123`), since that's how the check knows which issue to look at.
|
||||
|
||||
**What assignment means.** Being assigned is a commitment on both sides: we'll review your work seriously, and you'll see it through. That means responding to review feedback yourself and being able to explain any part of your change and why you made it that way. Use whatever tooling you like to get there — but if you can't answer a question about your own diff, we'll unassign the issue so someone else can pick it up.
|
||||
|
||||
**Bug fixes** — PRs are welcome for simple, well-scoped bug fixes where the problem and solution are both straightforward. "The function raises `TypeError` when passed `None` because of a missing guard" is a good candidate. If the fix requires design decisions or touches multiple subsystems, open an issue with a design proposal instead.
|
||||
|
||||
|
|
@ -34,7 +44,10 @@ An open issue is not an invitation to submit a PR. Issues track problems; whethe
|
|||
|
||||
If you do open a PR:
|
||||
|
||||
- **Reference an issue you're assigned to.** Every PR must reference a tracked issue using an auto-close keyword (`Fixes #123`, `Closes #123`, or `Resolves #123`), and the referenced issue must be assigned to you. If there isn't an issue, open one; then comment to ask a maintainer to assign it to you. This lets us deconflict effort and steer the approach before you invest time in code. External PRs that don't meet both conditions are automatically labeled `missing-issue-link` and closed; they reopen automatically once the link is present and you're assigned.
|
||||
- **Reference an issue you're assigned to.** Every PR must reference a tracked issue using an auto-close keyword (`Fixes #123`, `Closes #123`, or `Resolves #123`), and the referenced issue must be assigned to you — unless it's labeled `prs welcome`, which waives the assignment requirement. If there isn't an issue, open one. This lets us deconflict effort and steer the approach before you invest time in code. External PRs that don't meet these conditions are automatically labeled `missing-issue-link` and closed; they reopen automatically once the link is present and you're assigned.
|
||||
- **Leave "Allow edits by maintainers" enabled.** We frequently take a PR the last few steps ourselves rather than block on another round trip — tightening a test, adjusting naming, rebasing. It's enabled by default on PRs from personal forks; leave it that way. GitHub doesn't allow it at all for forks owned by an organization, so if you're contributing from one, expect us to land the final changes separately.
|
||||
- **Target the right branch.** Open against `main` unless you're fixing something specific to a maintenance line, in which case target that branch directly (`release/3.x`, `release/2.x`).
|
||||
- **If your PR was auto-closed, don't open a new one.** Edit the *existing* PR to add the issue link, get assigned to that issue, and it reopens on its own — the branch and history are preserved. A duplicate PR just starts you over and adds to the triage pile.
|
||||
- **Keep it focused.** One logical change per PR. Don't bundle unrelated fixes or refactors.
|
||||
- **Match existing patterns.** Follow the code style, type annotation conventions, and test patterns you see in the codebase. Run `uv run prek run --all-files` before submitting.
|
||||
- **Write tests.** Bug fixes should include a test that fails without the fix. Enhancements should include tests for the new behavior.
|
||||
|
|
|
|||
|
|
@ -29,11 +29,9 @@ When you mark a tool with `app=True` or `@app.ui()`, FastMCP wires up the metada
|
|||
|
||||
### The `app=True` flag
|
||||
|
||||
`app` on `@mcp.tool` accepts `True`, an `AppConfig`, or a dict. When you pass `True`, FastMCP checks whether the tool's return type is a Prefab type (`PrefabApp`, `Component`, or unions containing them). If it qualifies, FastMCP expands `True` into a full `AppConfig` — setting the renderer URI, CSP headers, and visibility — and stores it in the tool's `meta["ui"]` dict.
|
||||
`app` on `@mcp.tool` accepts `True`, an `AppConfig`, or a dict. When you pass `True`, FastMCP explicitly marks the tool as a Prefab UI tool and stamps placeholder UI metadata so the provider can synthesize the correct renderer resource later. When you omit `app`, FastMCP only applies this automatically if the tool's return type is a Prefab type (`PrefabApp`, `Component`, or unions containing them).
|
||||
|
||||
This expansion also registers the shared Prefab renderer resource (below). The tool and the renderer are linked through a `resourceUri` field in the metadata: the tool says "render me with `ui://prefab/renderer.html`" and the host fetches that resource when it displays the result.
|
||||
|
||||
Type inference works the same way. If the return type is a Prefab type and you haven't set `app` explicitly, FastMCP auto-wires the metadata as if you'd written `app=True`.
|
||||
The tool and renderer are linked through a `resourceUri` field in the metadata. Internally, registration uses the placeholder URI `ui://prefab/renderer.html`; when tools and resources are listed or read, FastMCP rewrites that placeholder to a per-tool URI like `ui://prefab/tool/<hash>/renderer.html` and synthesizes the matching renderer resource on demand.
|
||||
|
||||
### FastMCPApp registration
|
||||
|
||||
|
|
@ -49,13 +47,13 @@ When a Prefab tool runs, its return value — a `PrefabApp` or a bare `Component
|
|||
|
||||
The entry point is `PrefabApp.to_json()`. It walks the component tree and produces a JSON object with three top-level keys: `view` (the component tree), `state` (initial state values), and `_meta` (routing metadata).
|
||||
|
||||
FastMCP passes a `tool_resolver` callback to `to_json()`. Whenever the tree contains a `CallTool` action that references a function (not a string), the resolver converts it to a `ResolvedTool` with the function's registered name. This is how `CallTool(save_contact)` becomes `CallTool("save_contact")` on the wire. The resolver also handles `unwrap_result` — a flag telling the renderer to unwrap single-value results from the `{"result": value}` envelope FastMCP uses for schema compliance.
|
||||
FastMCP passes a `tool_resolver` callback to `to_json()`. Whenever the tree contains a `CallTool` action that references a function (not a string), the resolver converts it to a `ResolvedTool` with the function's registered name. For `FastMCPApp` backend tools, that registered name is then wrapped in the deterministic hashed format described below. The resolver also handles `unwrap_result` — a flag telling the renderer to unwrap single-value results from the `{"result": value}` envelope FastMCP uses for schema compliance.
|
||||
|
||||
### The `_meta.fastmcp.app` tag
|
||||
### Hashed backend tool references
|
||||
|
||||
After `to_json()` produces the tree, FastMCP injects `_meta.fastmcp.app` with the app's name (if the tool belongs to a `FastMCPApp`). This tag rides along inside `structuredContent` all the way to the renderer.
|
||||
FastMCP still tags app tools with `meta["fastmcp"]["app"]`, but backend routing no longer depends on sending the app name through each tool call. During serialization, FastMCP passes a resolver to `PrefabApp.to_json()`. When the tree contains `CallTool(save_contact)`, the resolver turns it into a deterministic hashed name such as `<hash>_save_contact`, where the hash is derived from the app name and backend tool name.
|
||||
|
||||
When the renderer calls a backend tool, it includes `_meta.fastmcp.app` in the `CallTool` request. The server sees this tag and routes the call through a special path that bypasses transforms (below).
|
||||
That hashed name rides along inside `structuredContent` all the way to the renderer. When the renderer calls the backend tool, it sends the hashed tool name in the normal MCP `tools/call` request. The server recognizes that format and routes through the app-tool lookup path described below.
|
||||
|
||||
### ToolResult assembly
|
||||
|
||||
|
|
@ -65,27 +63,27 @@ The final tool result has two parts: `content` (a list of `TextContent` blocks f
|
|||
|
||||
Normal tool calls go through the provider chain, which applies transforms (namespace prefixes, visibility filters) before resolving by name. App UI calls need a different path.
|
||||
|
||||
### The `get_app_tool` bypass
|
||||
### The hashed lookup bypass
|
||||
|
||||
Backend tools are typically hidden from the model (`visibility=["app"]`). Visibility transforms would filter them out of normal resolution. And namespace transforms might rename them — `save_contact` becomes `contacts_save_contact` — while the renderer still uses the original name.
|
||||
Backend tools are typically hidden from the model (`visibility=["app"]`). Visibility transforms would filter them out of normal resolution. And namespace transforms might rename them — `save_contact` becomes `contacts_save_contact` — while the renderer needs a stable way to call the original backend.
|
||||
|
||||
`get_app_tool` solves both problems. When the server sees `_meta.fastmcp.app` on an incoming `CallTool` request, it calls `get_app_tool(app_name, tool_name)` instead of the normal `get_tool(name)`. This walks the provider tree directly, skipping transforms. It finds the tool by its original registered name and verifies that its `meta["fastmcp"]["app"]` matches the expected app.
|
||||
Hashed lookup solves both problems. FastMCP first tries normal tool resolution. If no visible tool matches and the requested name looks like `<hash>_<local_name>`, FastMCP calls `get_tool_by_hash(hash, local_name)`. That lookup walks the provider tree directly, skipping transforms. It finds an app-visible tool by its original registered name and verifies that its stored `meta["fastmcp"]["_tool_hash"]` matches the requested hash.
|
||||
|
||||
That's why `CallTool("save_contact")` keeps working when the server is mounted under a namespace. The renderer sends the original name plus the app identity; the server uses `get_app_tool` to find it without transforms in the way.
|
||||
That's why `CallTool(save_contact)` keeps working when the server is mounted under a namespace. The renderer sends a deterministic hashed backend name; the server uses `get_tool_by_hash` to find the original tool without transforms in the way.
|
||||
|
||||
Authorization still applies. `get_app_tool` bypasses transforms but runs auth checks against the tool's `auth` config before executing.
|
||||
Authorization still applies. The hashed bypass skips name and visibility transforms, but auth checks still run against the tool's `auth` config before execution.
|
||||
|
||||
### Provider delegation
|
||||
|
||||
`get_app_tool` is defined on the `Provider` base class and overridden by aggregate and wrapped providers. Aggregate providers fan out the lookup across child providers in parallel. Wrapped providers (like `FastMCPProvider`, which wraps a nested `FastMCP` server) delegate to the inner server's `get_app_tool`. Backend tools are reachable through any depth of composition.
|
||||
`get_tool_by_hash` is defined on the `Provider` base class and overridden by aggregate and wrapped providers. Aggregate providers fan out the lookup across child providers in parallel. Wrapped providers (like `FastMCPProvider`, which wraps a nested `FastMCP` server) delegate to the inner server's hashed lookup. Backend tools are reachable through any depth of composition.
|
||||
|
||||
## The renderer
|
||||
|
||||
The Prefab renderer is a self-contained JavaScript application that interprets the JSON component tree and renders it as a React UI.
|
||||
|
||||
### The shared resource
|
||||
### Renderer resources
|
||||
|
||||
FastMCP registers the renderer as a `ui://prefab/renderer.html` resource with MIME type `text/html;profile=mcp-app`. The HTML is bundled inside the `prefab-ui` Python package; `get_renderer_html()` returns it as a string. All Prefab tools on a server share this single resource.
|
||||
FastMCP exposes the renderer through per-tool resources such as `ui://prefab/tool/<hash>/renderer.html`, each with MIME type `text/html;profile=mcp-app`. The HTML is bundled inside the `prefab-ui` Python package; `get_renderer_html()` returns it as a string. The resources are synthesized on demand from each tool's UI metadata, so CSP and permissions can differ per tool even though they use the same Prefab renderer.
|
||||
|
||||
The resource also carries CSP metadata (via `get_renderer_csp()`) declaring the CDN domains the renderer needs. Hosts use this to configure the iframe's Content Security Policy.
|
||||
|
||||
|
|
@ -93,7 +91,7 @@ The resource also carries CSP metadata (via `get_renderer_csp()`) declaring the
|
|||
|
||||
The renderer lives in a sandboxed iframe and communicates with the host using `postMessage`. The protocol follows the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) spec:
|
||||
|
||||
The host pushes the tool result (with `structuredContent`) into the iframe. The renderer parses the component tree, initializes state, and renders the UI. When the user interacts — submitting a form, clicking a button — and the interaction triggers a `CallTool` action, the renderer sends a `callServerTool` message back to the host via `postMessage`. The host forwards it as a regular MCP `tools/call` request to the server, including `_meta.fastmcp.app` for routing.
|
||||
The host pushes the tool result (with `structuredContent`) into the iframe. The renderer parses the component tree, initializes state, and renders the UI. When the user interacts — submitting a form, clicking a button — and the interaction triggers a `CallTool` action, the renderer sends a `callServerTool` message back to the host via `postMessage`. The host forwards it as a regular MCP `tools/call` request to the server, using the hashed backend name that FastMCP serialized into the action.
|
||||
|
||||
The response flows back the same way: server → host → iframe via `postMessage`, and the renderer updates state with the result.
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ fastmcp dev apps server.py:mcp --mcp-port 9000 --dev-port 9090 --no-reload
|
|||
| MCP Port | `--mcp-port` | `8000` | Port for your MCP server |
|
||||
| Dev Port | `--dev-port` | `8080` | Port for the dev UI |
|
||||
| Auto-Reload | `--reload` / `--no-reload` | On | Watch files and restart the server on changes |
|
||||
| Host | `--host` | `127.0.0.1` | Interface for both local servers to bind |
|
||||
| Log Panel | `--log-panel` / `--no-log-panel` | On | Show or hide the log panel in the dev UI |
|
||||
|
||||
## Multiple tools
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ def chart_view() -> str:
|
|||
|
||||
## AppConfig
|
||||
|
||||
`AppConfig` controls how a tool or resource participates in the Apps extension. Import it from `fastmcp.server.apps`:
|
||||
`AppConfig` controls how a tool or resource participates in the Apps extension. Import it from `fastmcp.apps`:
|
||||
|
||||
```python
|
||||
from fastmcp.apps import AppConfig
|
||||
|
|
@ -212,11 +212,11 @@ import base64
|
|||
import io
|
||||
|
||||
import qrcode
|
||||
from mcp import types
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps import AppConfig, ResourceCSP
|
||||
from fastmcp.tools import ToolResult
|
||||
from mcp_types import ImageContent
|
||||
|
||||
mcp = FastMCP("QR Code Server")
|
||||
|
||||
|
|
@ -236,7 +236,7 @@ def generate_qr(text: str = "https://gofastmcp.com") -> ToolResult:
|
|||
b64 = base64.b64encode(buffer.getvalue()).decode()
|
||||
|
||||
return ToolResult(
|
||||
content=[types.ImageContent(type="image", data=b64, mimeType="image/png")]
|
||||
content=[ImageContent(type="image", data=b64, mime_type="image/png")]
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,25 @@ rss: true
|
|||
tag: NEW
|
||||
---
|
||||
|
||||
<Update label="v3.4.4" description="2026-07-08">
|
||||
|
||||
**[v3.4.4: Host in Translation](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.4)**
|
||||
|
||||
FastMCP 3.4.4 restores HTTP deployment compatibility after the 3.4.3 Host/Origin guard changed default behavior for existing ASGI, serverless, and reverse-proxy deployments. The guard implementation remains available for deployments that opt in with explicit trusted hosts and origins, while 3.x returns to accepting traffic that worked before the patch. This release also adds Hugging Face OAuth provider support, with docs and examples for public and private apps, PKCE, Dynamic Client Registration, and CIMD.
|
||||
|
||||
### Enhancements ✨
|
||||
* Hugging Face Auth Integration by [@evalstate](https://github.com/evalstate) in [#4385](https://github.com/PrefectHQ/fastmcp/pull/4385)
|
||||
### Fixes 🐞
|
||||
* Relax host origin guard defaults by [@jlowin](https://github.com/jlowin) in [#4439](https://github.com/PrefectHQ/fastmcp/pull/4439)
|
||||
* Restore HTTP host guard compatibility by [@jlowin](https://github.com/jlowin) in [#4472](https://github.com/PrefectHQ/fastmcp/pull/4472)
|
||||
|
||||
## New Contributors
|
||||
* @evalstate made their first contribution in [#4385](https://github.com/PrefectHQ/fastmcp/pull/4385)
|
||||
|
||||
**Full Changelog**: [v3.4.3...v3.4.4](https://github.com/PrefectHQ/fastmcp/compare/v3.4.3...v3.4.4)
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="v3.4.3" description="2026-07-05">
|
||||
|
||||
**[v3.4.3: The Fast and the Secure-ious](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.3)**
|
||||
|
|
@ -616,7 +635,7 @@ FastMCP 3.2 is the Apps release: your tools can now return interactive UIs — c
|
|||
* Add tag to docs by [@jlowin](https://github.com/jlowin) in [#3382](https://github.com/PrefectHQ/fastmcp/pull/3382)
|
||||
* Add settings and environment variables reference by [@jlowin](https://github.com/jlowin) in [#3384](https://github.com/PrefectHQ/fastmcp/pull/3384)
|
||||
* Add contributing guidelines and update issue/PR templates by [@jlowin](https://github.com/jlowin) in [#3485](https://github.com/PrefectHQ/fastmcp/pull/3485)
|
||||
* [Documentation] Move stateless_http transport kwarg to http_app as FastMCP constructo… by [@mhallo](https://github.com/mhallo) in [#3510](https://github.com/PrefectHQ/fastmcp/pull/3510)
|
||||
* [Documentation] Move stateless_http transport kwarg to http_app as FastMCP constructor… by [@mhallo](https://github.com/mhallo) in [#3510](https://github.com/PrefectHQ/fastmcp/pull/3510)
|
||||
* Update security policy by [@jlowin](https://github.com/jlowin) in [#3521](https://github.com/PrefectHQ/fastmcp/pull/3521)
|
||||
* Add release instructions to CLAUDE.md by [@jlowin](https://github.com/jlowin) in [#3583](https://github.com/PrefectHQ/fastmcp/pull/3583)
|
||||
* fix(docs): correct misleading stateless_http header by [@jlowin](https://github.com/jlowin) in [#3622](https://github.com/PrefectHQ/fastmcp/pull/3622)
|
||||
|
|
@ -1948,7 +1967,7 @@ Thank you to our new contributors and everyone who tested preview builds. Your f
|
|||
* Add configurable redirect URI validation for OAuth providers by [@jlowin](https://github.com/jlowin) in [#1582](https://github.com/PrefectHQ/fastmcp/pull/1582)
|
||||
* Remove invalid-argument-type ignore and fix type errors by [@jlowin](https://github.com/jlowin) in [#1588](https://github.com/PrefectHQ/fastmcp/pull/1588)
|
||||
* Remove generate-schema from public CLI by [@jlowin](https://github.com/jlowin) in [#1591](https://github.com/PrefectHQ/fastmcp/pull/1591)
|
||||
* Skip flaky windows test / mulit-client garbage collection by [@jlowin](https://github.com/jlowin) in [#1592](https://github.com/PrefectHQ/fastmcp/pull/1592)
|
||||
* Skip flaky windows test / multi-client garbage collection by [@jlowin](https://github.com/jlowin) in [#1592](https://github.com/PrefectHQ/fastmcp/pull/1592)
|
||||
* Add setting to disable logging configuration by [@isra17](https://github.com/isra17) in [#1575](https://github.com/PrefectHQ/fastmcp/pull/1575)
|
||||
* Improve debug logging for nested Servers / Clients by [@strawgate](https://github.com/strawgate) in [#1604](https://github.com/PrefectHQ/fastmcp/pull/1604)
|
||||
* Add GitHub pull request template by [@strawgate](https://github.com/strawgate) in [#1581](https://github.com/PrefectHQ/fastmcp/pull/1581)
|
||||
|
|
@ -3737,4 +3756,4 @@ This release is highlighted by the ability to handle complex JSON objects as MCP
|
|||
The very first release of FastMCP! 🎉
|
||||
|
||||
**Full Changelog**: [Initial commits](https://github.com/PrefectHQ/fastmcp/commits/v0.1.0)
|
||||
</Update>
|
||||
</Update>
|
||||
|
|
|
|||
|
|
@ -23,21 +23,24 @@ fastmcp auth cimd create \
|
|||
|
||||
```json
|
||||
{
|
||||
"client_id": "https://your-domain.com/oauth/client.json",
|
||||
"client_id": "https://YOUR-DOMAIN.com/path/to/client.json",
|
||||
"client_name": "My App",
|
||||
"redirect_uris": ["http://localhost:*/callback"],
|
||||
"token_endpoint_auth_method": "none"
|
||||
"token_endpoint_auth_method": "none",
|
||||
"grant_types": ["authorization_code"],
|
||||
"response_types": ["code"]
|
||||
}
|
||||
```
|
||||
|
||||
The generated document includes a placeholder `client_id` — update it to match the URL where you'll host the document before deploying.
|
||||
By default, the generated document includes a placeholder `client_id`. Update it to match the URL where you'll host the document before deploying, or pass `--client-id` when generating the file.
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Flag | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| Name | `--name` | **Required.** Human-readable client name |
|
||||
| Redirect URI | `--redirect-uri` | **Required.** Allowed redirect URIs (repeatable) |
|
||||
| Redirect URI | `--redirect-uri`, `-r` | **Required.** Allowed redirect URIs (repeatable) |
|
||||
| Client ID | `--client-id` | URL where this document will be hosted; defaults to a placeholder |
|
||||
| Client URI | `--client-uri` | Client's home page URL |
|
||||
| Logo URI | `--logo-uri` | Client's logo URL |
|
||||
| Scope | `--scope` | Space-separated list of scopes |
|
||||
|
|
@ -51,6 +54,7 @@ fastmcp auth cimd create \
|
|||
--name "My Production App" \
|
||||
--redirect-uri "http://localhost:*/callback" \
|
||||
--redirect-uri "https://myapp.example.com/callback" \
|
||||
--client-id "https://myapp.example.com/oauth/client.json" \
|
||||
--client-uri "https://myapp.example.com" \
|
||||
--scope "read write" \
|
||||
--output client.json
|
||||
|
|
|
|||
|
|
@ -104,11 +104,28 @@ Some tools request additional input during execution through MCP's elicitation m
|
|||
| ------ | ---- | ----------- |
|
||||
| Command | `--command` | Connect via stdio |
|
||||
| Transport | `--transport`, `-t` | Force `http` or `sse` |
|
||||
| Prompt | `--prompt` | Treat the target as a prompt name instead of a tool/resource |
|
||||
| Input JSON | `--input-json` | Base arguments as JSON (merged with `key=value`) |
|
||||
| JSON | `--json` | Raw JSON output |
|
||||
| Timeout | `--timeout` | Connection timeout in seconds |
|
||||
| Auth | `--auth` | `oauth`, a bearer token, or `none` |
|
||||
|
||||
## Reading Resources and Getting Prompts
|
||||
|
||||
`fastmcp call` can also read resources and render prompts. If the target contains `://`, the CLI treats it as a resource URI and calls `read_resource`:
|
||||
|
||||
```bash
|
||||
fastmcp call server.py resource://docs/readme
|
||||
fastmcp call server.py file:///tmp/example.txt --json
|
||||
```
|
||||
|
||||
To get a prompt, pass `--prompt`; prompt arguments use the same `key=value` and `--input-json` forms as tool calls:
|
||||
|
||||
```bash
|
||||
fastmcp call server.py summarize --prompt topic=weather
|
||||
fastmcp call server.py summarize --prompt --input-json '{"topic": "weather"}'
|
||||
```
|
||||
|
||||
## Discovering Configured Servers
|
||||
|
||||
`fastmcp discover` scans your machine for MCP servers configured in editors and tools. It checks:
|
||||
|
|
|
|||
|
|
@ -55,6 +55,11 @@ fastmcp inspect server.py --format mcp -o manifest.json
|
|||
| ------ | ---- | ----------- |
|
||||
| Format | `--format`, `-f` | `fastmcp` or `mcp` (required when using `-o`) |
|
||||
| Output File | `--output`, `-o` | Save to file instead of stdout |
|
||||
| Python | `--python` | Python version to use when running via `uv` |
|
||||
| Extra Packages | `--with` | Additional packages to install (repeatable) |
|
||||
| Project | `--project` | Run within a specific uv project directory |
|
||||
| Requirements | `--with-requirements` | Install from a requirements file |
|
||||
| Skip Env | `--skip-env` | Do not set up a uv environment |
|
||||
|
||||
## Entrypoints
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
|
|||
```bash
|
||||
fastmcp install claude-desktop server.py
|
||||
fastmcp install claude-code server.py --with pandas --with matplotlib
|
||||
fastmcp install cursor server.py -e .
|
||||
fastmcp install cursor server.py --with-editable .
|
||||
```
|
||||
|
||||
<Warning>
|
||||
|
|
@ -41,14 +41,13 @@ Because MCP clients run servers in isolation, you need to tell the install comma
|
|||
|
||||
```bash
|
||||
fastmcp install claude-desktop server.py --with pandas --with "sqlalchemy>=2.0"
|
||||
fastmcp install cursor server.py -e . --with-requirements requirements.txt
|
||||
fastmcp install cursor server.py --with-editable . --with-requirements requirements.txt
|
||||
```
|
||||
|
||||
**`fastmcp.json`** configuration files declare dependencies alongside the server definition. When you install from a config file, dependencies are picked up automatically:
|
||||
**`fastmcp.json`** configuration files declare dependencies alongside the server definition. When you install from a config file explicitly, dependencies are picked up automatically:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-desktop fastmcp.json
|
||||
fastmcp install claude-desktop # auto-detects fastmcp.json in current directory
|
||||
```
|
||||
|
||||
See [Server Configuration](/deployment/server-configuration) for the full config format.
|
||||
|
|
@ -57,15 +56,19 @@ See [Server Configuration](/deployment/server-configuration) for the full config
|
|||
|
||||
| Option | Flag | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| Server Name | `--server-name`, `-n` | Custom name for the server |
|
||||
| Editable Package | `--with-editable`, `-e` | Install a directory in editable mode |
|
||||
| Server Name | `--name`, `-n` | Custom name for the server |
|
||||
| Editable Package | `--with-editable` | Install a directory in editable mode |
|
||||
| Extra Packages | `--with` | Additional packages (repeatable) |
|
||||
| Environment Variables | `--env` | `KEY=VALUE` pairs (repeatable) |
|
||||
| Environment File | `--env-file`, `-f` | Load env vars from a `.env` file |
|
||||
| Environment File | `--env-file` | Load env vars from a `.env` file |
|
||||
| Python | `--python` | Python version (e.g., `3.11`) |
|
||||
| Project | `--project` | Run within a uv project directory |
|
||||
| Requirements | `--with-requirements` | Install from a requirements file |
|
||||
| Config Path | `--config-path` | Custom path to Claude Desktop config directory (`claude-desktop` only) |
|
||||
| Workspace | `--workspace` | Install to the workspace directory instead of globally (`cursor` only) |
|
||||
| Copy | `--copy` | Copy the generated output to the clipboard (`mcp-json` and `stdio` only) |
|
||||
|
||||
`goose` installs through a deeplink that runs your server with `uvx`, so it accepts only `--name`, `--with`, and `--python`. Options that depend on a local uv project — `--with-editable`, `--project`, and `--with-requirements` — are unavailable there. Deeplinks also cannot carry environment variables: passing `--env` or `--env-file` exits with an error directing you to `fastmcp install mcp-json`, which generates a config you can add to Goose by hand with the variables included.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
@ -73,12 +76,12 @@ See [Server Configuration](/deployment/server-configuration) for the full config
|
|||
# Basic install with auto-detected server instance
|
||||
fastmcp install claude-desktop server.py
|
||||
|
||||
# Install from fastmcp.json with auto-detection
|
||||
fastmcp install claude-desktop
|
||||
# Install from fastmcp.json
|
||||
fastmcp install claude-desktop fastmcp.json
|
||||
|
||||
# Explicit entrypoint with dependencies
|
||||
fastmcp install claude-desktop server.py:my_server \
|
||||
--server-name "My Analysis Server" \
|
||||
--name "My Analysis Server" \
|
||||
--with pandas
|
||||
|
||||
# With environment variables
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ fastmcp --help
|
|||
| [`install`](/cli/install-mcp) | Install a server into Claude Code, Claude Desktop, Cursor, Gemini CLI, or Goose |
|
||||
| [`inspect`](/cli/inspecting) | Print a server's tools, resources, and prompts as a summary or JSON report |
|
||||
| [`list`](/cli/client) | List a server's tools (and optionally resources and prompts) |
|
||||
| [`call`](/cli/client#calling-tools) | Call a single tool with arguments |
|
||||
| [`call`](/cli/client#calling-tools) | Call a tool, read a resource, or get a prompt |
|
||||
| [`discover`](/cli/client#discovering-configured-servers) | Find MCP servers configured in your editors and tools |
|
||||
| [`generate-cli`](/cli/generate-cli) | Scaffold a standalone typed CLI from a server's tool schemas |
|
||||
| [`project prepare`](/cli/running#pre-building-environments) | Pre-install dependencies into a reusable uv project |
|
||||
|
|
|
|||
|
|
@ -69,19 +69,22 @@ fastmcp run mcp.json
|
|||
```
|
||||
|
||||
<Warning>
|
||||
`fastmcp run` completely ignores the `if __name__ == "__main__"` block. Any setup code in that block won't execute. If you need initialization logic to run, use a [factory function](/cli/overview#factory-functions).
|
||||
`fastmcp run` completely ignores the `if __name__ == "__main__"` block. Any setup code in that block won't execute. If you need initialization logic to run, use a [factory function](#entrypoints).
|
||||
</Warning>
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Flag | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| Transport | `--transport`, `-t` | `stdio` (default), `http`, or `sse` |
|
||||
| Transport | `--transport`, `-t` | `stdio` (default), `http` / `streamable-http`, or `sse` |
|
||||
| Host | `--host` | Bind address for HTTP (default: `127.0.0.1`) |
|
||||
| Port | `--port`, `-p` | Bind port for HTTP (default: `8000`) |
|
||||
| Path | `--path` | URL path for HTTP (default: `/mcp/`) |
|
||||
| Path | `--path` | URL path for HTTP (default: `/mcp` for `http`, `/sse` for `sse`) |
|
||||
| Log Level | `--log-level`, `-l` | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
|
||||
| No Banner | `--no-banner` | Suppress the startup banner |
|
||||
| Stateless | `--stateless` | Run without sessions, for serverless and multi-worker deployments |
|
||||
| Module Mode | `--module`, `-m` | Run a Python module via `python -m` instead of a file path |
|
||||
| Skip Source | `--skip-source` | Skip source preparation (use when the source is already prepared) |
|
||||
| Auto-Reload | `--reload` / `--no-reload` | Watch for file changes and restart automatically |
|
||||
| Reload Dirs | `--reload-dir` | Directories to watch (repeatable) |
|
||||
| Skip Env | `--skip-env` | Don't set up a uv environment (use when already in one) |
|
||||
|
|
@ -127,7 +130,7 @@ Auto-reload is on by default — save a file and the MCP server restarts automat
|
|||
|
||||
```bash
|
||||
fastmcp dev inspector server.py
|
||||
fastmcp dev inspector server.py -e . --with pandas
|
||||
fastmcp dev inspector server.py --with-editable . --with pandas
|
||||
```
|
||||
|
||||
<Tip>
|
||||
|
|
@ -140,7 +143,7 @@ The Inspector connects over **stdio only**. When it launches, you may need to se
|
|||
|
||||
| Option | Flag | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| Editable Package | `--with-editable`, `-e` | Install a directory in editable mode |
|
||||
| Editable Package | `--with-editable` | Install a directory in editable mode |
|
||||
| Extra Packages | `--with` | Additional packages (repeatable) |
|
||||
| Inspector Version | `--inspector-version` | MCP Inspector version to use |
|
||||
| UI Port | `--ui-port` | Port for the Inspector UI |
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ async with Client(
|
|||
"https://your-server.fastmcp.app/mcp",
|
||||
auth="<your-token>",
|
||||
) as client:
|
||||
await client.ping()
|
||||
await client.list_tools()
|
||||
```
|
||||
|
||||
You can also supply a Bearer token to a transport instance, such as `StreamableHttpTransport` or `SSETransport`:
|
||||
|
|
@ -52,12 +52,12 @@ transport = StreamableHttpTransport(
|
|||
)
|
||||
|
||||
async with Client(transport) as client:
|
||||
await client.ping()
|
||||
await client.list_tools()
|
||||
```
|
||||
|
||||
## `BearerAuth` Helper
|
||||
|
||||
If you prefer to be more explicit and not rely on FastMCP to transform your string token, you can use the `BearerAuth` class yourself, which implements the `httpx.Auth` interface.
|
||||
If you prefer to be more explicit and not rely on FastMCP to transform your string token, you can use the `BearerAuth` class yourself, which implements the `httpx2.Auth` interface.
|
||||
|
||||
```python {6}
|
||||
from fastmcp import Client
|
||||
|
|
@ -67,7 +67,7 @@ async with Client(
|
|||
"https://your-server.fastmcp.app/mcp",
|
||||
auth=BearerAuth(token="<your-token>"),
|
||||
) as client:
|
||||
await client.ping()
|
||||
await client.list_tools()
|
||||
```
|
||||
|
||||
## Custom Headers
|
||||
|
|
@ -84,5 +84,5 @@ async with Client(
|
|||
headers={"X-API-Key": "<your-token>"},
|
||||
),
|
||||
) as client:
|
||||
await client.ping()
|
||||
await client.list_tools()
|
||||
```
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ async with Client(
|
|||
client_metadata_url="https://myapp.example.com/oauth/client.json",
|
||||
),
|
||||
) as client:
|
||||
await client.ping()
|
||||
await client.list_tools()
|
||||
```
|
||||
|
||||
When the server supports CIMD, the client uses your metadata URL as its `client_id` instead of performing Dynamic Client Registration. The server fetches your document, validates it, and proceeds with the standard OAuth authorization flow.
|
||||
|
|
|
|||
89
docs/clients/auth/client-credentials.mdx
Normal file
89
docs/clients/auth/client-credentials.mdx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
---
|
||||
title: Machine-to-Machine Authentication
|
||||
sidebarTitle: Client Credentials
|
||||
description: Authenticate your FastMCP client to a protected server without a browser.
|
||||
icon: robot
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="4.0.0" />
|
||||
|
||||
<Tip>
|
||||
Machine-to-machine authentication is only relevant for HTTP-based transports.
|
||||
</Tip>
|
||||
|
||||
When a FastMCP client runs without a human present — a backend service, a scheduled job, a CI pipeline, one MCP server calling another — it cannot complete the browser-based [OAuth](/clients/auth/oauth) flow. Instead it authenticates as itself using the OAuth 2.0 **client credentials** grant: the client presents its own credentials directly to the authorization server, receives an access token, and attaches that token to every request. There is no redirect, no consent screen, and no user.
|
||||
|
||||
FastMCP provides two providers for this, both implementing the `httpx2.Auth` interface so they drop into the same `auth=` parameter as every other client auth option. You pass the **MCP server URL**, not a token endpoint — the token endpoint is discovered from the server's OAuth metadata, exactly as the interactive `OAuth` helper does. As with `OAuth`, you can omit the URL entirely and let the transport supply it.
|
||||
|
||||
## Client ID and Secret
|
||||
|
||||
The common case is a pre-registered client with an ID and a secret. Use `ClientCredentialsOAuthProvider` and pass it to the `auth` parameter of your `Client` or transport:
|
||||
|
||||
```python {2, 4-8, 10}
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.auth import ClientCredentialsOAuthProvider
|
||||
|
||||
auth = ClientCredentialsOAuthProvider(
|
||||
client_id="my-client-id",
|
||||
client_secret="my-client-secret",
|
||||
scopes=["read", "write"],
|
||||
)
|
||||
|
||||
async with Client("https://example.com/mcp", auth=auth) as client:
|
||||
await client.list_tools()
|
||||
```
|
||||
|
||||
The provider discovers the authorization server, exchanges the credentials for an access token, and caches the token in memory for the life of the client. When the token expires it is re-acquired automatically on the next request. Because re-acquiring a token is a single non-interactive request, tokens are held in memory by default with no warning — unlike the interactive `OAuth` flow, losing the cache on restart costs nothing.
|
||||
|
||||
### `ClientCredentialsOAuthProvider` Parameters
|
||||
|
||||
- **`mcp_url`** (`str`, optional): Full URL to the MCP endpoint. Omit it when passing the provider to `Client(auth=...)` — the transport supplies the URL automatically.
|
||||
- **`client_id`** (`str`, required): The pre-registered OAuth client ID.
|
||||
- **`client_secret`** (`str`, required): The OAuth client secret.
|
||||
- **`scopes`** (`str | list[str]`, optional): Scopes to request, as a space-separated string or a list.
|
||||
- **`token_endpoint_auth_method`** (`"client_secret_basic" | "client_secret_post"`, optional): How the credentials are presented to the token endpoint. Defaults to `"client_secret_basic"` (an HTTP Basic `Authorization` header); use `"client_secret_post"` to send them in the request body instead.
|
||||
- **`token_storage`** (`AsyncKeyValue`, optional): A key-value store for the acquired token. Defaults to in-memory storage.
|
||||
|
||||
## Private Key JWT
|
||||
|
||||
Some authorization servers require the client to prove its identity with a signed JWT assertion (RFC 7523 `private_key_jwt`) instead of a shared secret. This is common with workload identity federation, where the assertion comes from a cloud identity provider. Use `PrivateKeyJWTOAuthProvider` and supply an `assertion_provider` — an async callback that receives the authorization server's issuer identifier (the required JWT audience) and returns the assertion.
|
||||
|
||||
For a locally signed assertion, build the callback with `SignedJWTParameters`:
|
||||
|
||||
```python {4-7, 9, 11-15, 17-20, 22}
|
||||
from pathlib import Path
|
||||
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.auth import (
|
||||
PrivateKeyJWTOAuthProvider,
|
||||
SignedJWTParameters,
|
||||
)
|
||||
|
||||
private_key_pem = Path("client-signing-key.pem").read_text()
|
||||
|
||||
jwt_params = SignedJWTParameters(
|
||||
issuer="my-client-id",
|
||||
subject="my-client-id",
|
||||
signing_key=private_key_pem,
|
||||
)
|
||||
|
||||
auth = PrivateKeyJWTOAuthProvider(
|
||||
client_id="my-client-id",
|
||||
assertion_provider=jwt_params.create_assertion_provider(),
|
||||
)
|
||||
|
||||
async with Client("https://example.com/mcp", auth=auth) as client:
|
||||
await client.list_tools()
|
||||
```
|
||||
|
||||
If you already have a JWT from an identity provider, wrap it with `static_assertion_provider`, or pass your own `async def provider(audience: str) -> str` callback to fetch one on demand.
|
||||
|
||||
### `PrivateKeyJWTOAuthProvider` Parameters
|
||||
|
||||
- **`mcp_url`** (`str`, optional): Full URL to the MCP endpoint. Omit it when passing the provider to `Client(auth=...)`.
|
||||
- **`client_id`** (`str`, required): The OAuth client ID.
|
||||
- **`assertion_provider`** (`Callable[[str], Awaitable[str]]`, required): Async callback that receives the authorization server's issuer identifier and returns a signed JWT assertion.
|
||||
- **`scopes`** (`str | list[str]`, optional): Scopes to request, as a space-separated string or a list.
|
||||
- **`token_storage`** (`AsyncKeyValue`, optional): A key-value store for the acquired token. Defaults to in-memory storage.
|
||||
|
|
@ -29,13 +29,13 @@ from fastmcp import Client
|
|||
|
||||
# Uses default OAuth settings
|
||||
async with Client("https://your-server.fastmcp.app/mcp", auth="oauth") as client:
|
||||
await client.ping()
|
||||
await client.list_tools()
|
||||
```
|
||||
|
||||
|
||||
### `OAuth` Helper
|
||||
|
||||
To fully configure the OAuth flow, use the `OAuth` helper and pass it to the `auth` parameter of the `Client` or transport instance. `OAuth` manages the complexities of the OAuth 2.1 Authorization Code Grant with PKCE (Proof Key for Code Exchange) for enhanced security, and implements the full `httpx.Auth` interface.
|
||||
To fully configure the OAuth flow, use the `OAuth` helper and pass it to the `auth` parameter of the `Client` or transport instance. `OAuth` manages the complexities of the OAuth 2.1 Authorization Code Grant with PKCE (Proof Key for Code Exchange) for enhanced security, and implements the full `httpx2.Auth` interface.
|
||||
|
||||
```python {2, 4, 6}
|
||||
from fastmcp import Client
|
||||
|
|
@ -44,7 +44,7 @@ from fastmcp.client.auth import OAuth
|
|||
oauth = OAuth(scopes=["user"])
|
||||
|
||||
async with Client("https://your-server.fastmcp.app/mcp", auth=oauth) as client:
|
||||
await client.ping()
|
||||
await client.list_tools()
|
||||
```
|
||||
|
||||
<Note>
|
||||
|
|
@ -61,7 +61,7 @@ You don't need to pass `mcp_url` when using `OAuth` with `Client(auth=...)` —
|
|||
- **`token_storage`** (`AsyncKeyValue`, optional): Storage backend for persisting OAuth tokens. Defaults to in-memory storage (tokens lost on restart). See [Token Storage](#token-storage) for encrypted storage options
|
||||
- **`additional_client_metadata`** (`dict[str, Any]`, optional): Extra metadata for client registration
|
||||
- **`callback_port`** (`int`, optional): Fixed port for OAuth callback server. If not specified, uses a random available port
|
||||
- **`httpx_client_factory`** (`McpHttpClientFactory`, optional): Factory for creating httpx clients
|
||||
- **`httpx_client_factory`** (`McpHttpClientFactory`, optional): Factory for creating httpx2 clients
|
||||
|
||||
|
||||
## OAuth Flow
|
||||
|
|
@ -125,7 +125,7 @@ encrypted_storage = FernetEncryptionWrapper(
|
|||
oauth = OAuth(token_storage=encrypted_storage)
|
||||
|
||||
async with Client("https://your-server.fastmcp.app/mcp", auth=oauth) as client:
|
||||
await client.ping()
|
||||
await client.list_tools()
|
||||
```
|
||||
|
||||
You can use any `AsyncKeyValue`-compatible backend from the [key-value library](https://github.com/strawgate/py-key-value) including Redis, DynamoDB, and more. Wrap your storage in `FernetEncryptionWrapper` for encryption.
|
||||
|
|
@ -150,7 +150,7 @@ async with Client(
|
|||
client_metadata_url="https://myapp.example.com/oauth/client.json",
|
||||
),
|
||||
) as client:
|
||||
await client.ping()
|
||||
await client.list_tools()
|
||||
```
|
||||
|
||||
See the [CIMD Authentication](/clients/auth/cimd) page for complete documentation on creating, hosting, and validating CIMD documents.
|
||||
|
|
@ -172,7 +172,7 @@ async with Client(
|
|||
client_secret="my-client-secret",
|
||||
),
|
||||
) as client:
|
||||
await client.ping()
|
||||
await client.list_tools()
|
||||
```
|
||||
|
||||
Public clients that rely on PKCE for security can omit `client_secret`:
|
||||
|
|
|
|||
|
|
@ -37,9 +37,6 @@ client = Client("my_mcp_server.py")
|
|||
|
||||
async def main():
|
||||
async with client:
|
||||
# Basic server interaction
|
||||
await client.ping()
|
||||
|
||||
# List available operations
|
||||
tools = await client.list_tools()
|
||||
resources = await client.list_resources()
|
||||
|
|
@ -67,16 +64,21 @@ server = FastMCP("TestServer")
|
|||
client = Client(server) # In-memory, no network or subprocess
|
||||
```
|
||||
|
||||
**STDIO transport** launches a server as a subprocess and communicates through stdin/stdout pipes. This is the standard mechanism used by desktop clients like Claude Desktop. The subprocess runs in an isolated environment, so you must explicitly pass any environment variables the server needs.
|
||||
**STDIO transport** launches a server as a subprocess and communicates through stdin/stdout pipes. This is the standard mechanism used by desktop clients like Claude Desktop. By default, the subprocess receives the MCP SDK's default environment; pass an explicit transport when you need to add environment variables, set a working directory, or control process reuse.
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.transports import PythonStdioTransport
|
||||
|
||||
# Simple inference from file path
|
||||
client = Client("my_server.py")
|
||||
|
||||
# With explicit environment configuration
|
||||
client = Client("my_server.py", env={"API_KEY": "secret"})
|
||||
transport = PythonStdioTransport(
|
||||
"my_server.py",
|
||||
env={"API_KEY": "secret"},
|
||||
)
|
||||
client = Client(transport)
|
||||
```
|
||||
|
||||
**HTTP transport** connects to servers running as web services. Use this for production deployments where the server runs independently and manages its own lifecycle.
|
||||
|
|
@ -121,7 +123,7 @@ async with client:
|
|||
|
||||
## Connection Lifecycle
|
||||
|
||||
The client uses context managers for connection management. When you enter the context, the client establishes a connection and performs an MCP initialization handshake with the server. This handshake exchanges capabilities, server metadata, and instructions.
|
||||
The client uses context managers for connection management. When you enter the context, the client establishes a connection and negotiates the protocol era with the server. Metadata returned by either legacy initialization or modern discovery is exposed through the same client properties.
|
||||
|
||||
```python
|
||||
from fastmcp import Client, FastMCP
|
||||
|
|
@ -134,10 +136,12 @@ def greet(name: str) -> str:
|
|||
return f"Hello, {name}!"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
# Initialization already happened automatically
|
||||
print(f"Server: {client.initialize_result.serverInfo.name}")
|
||||
print(f"Instructions: {client.initialize_result.instructions}")
|
||||
print(f"Capabilities: {client.initialize_result.capabilities.tools}")
|
||||
# Protocol negotiation already happened automatically
|
||||
assert client.server_info is not None
|
||||
assert client.server_capabilities is not None
|
||||
print(f"Server: {client.server_info.name}")
|
||||
print(f"Instructions: {client.instructions}")
|
||||
print(f"Capabilities: {client.server_capabilities.tools}")
|
||||
```
|
||||
|
||||
For advanced scenarios where you need precise control over when initialization happens, disable automatic initialization and call `initialize()` manually:
|
||||
|
|
@ -154,12 +158,149 @@ async with client:
|
|||
|
||||
# Initialize manually with custom timeout
|
||||
result = await client.initialize(timeout=10.0)
|
||||
print(f"Server: {result.serverInfo.name}")
|
||||
print(f"Server: {result.server_info.name}")
|
||||
|
||||
# Now ready for operations
|
||||
tools = await client.list_tools()
|
||||
```
|
||||
|
||||
## Protocol negotiation
|
||||
|
||||
<VersionBadge version="4.0.0" />
|
||||
|
||||
MCP has two protocol eras: the original *legacy* era, which begins every connection with an `initialize` handshake, and the *modern* era (protocol version `2026-07-28` and later), which a client discovers by probing the server's `server/discover` endpoint. The `mode` parameter controls which era the client negotiates when it connects.
|
||||
|
||||
By default, `mode="auto"`. The client probes `server/discover` and adopts the modern protocol when the server responds; for any server that is not positive evidence of modern support, it falls back to the legacy handshake. This makes the default safe against a mixed fleet of legacy and modern servers.
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
# Negotiate the newest era the server supports (the default)
|
||||
client = Client("https://example.com/mcp", mode="auto")
|
||||
```
|
||||
|
||||
Set `mode="legacy"` to force the initialize handshake. This behaves identically to earlier FastMCP versions and is the opt-out if a server misbehaves under discovery or you need the legacy `initialize` result object.
|
||||
|
||||
```python
|
||||
client = Client("https://example.com/mcp", mode="legacy")
|
||||
```
|
||||
|
||||
Legacy mode is also what you need for the capabilities that depend on a live session between client and server. The handshake opens a persistent back-channel the server can push requests down, and the modern era removed it. Pin `mode="legacy"` when your code relies on any of these:
|
||||
|
||||
- **[Sampling](/clients/sampling)** — server-initiated LLM completion requests
|
||||
- **[Roots](/clients/roots)** — server-initiated requests for the client's roots
|
||||
- **[Elicitation](/clients/elicitation)** — server-initiated requests for user input, which modern connections replace with [input-required rounds](/clients/elicitation#input-required-rounds)
|
||||
- `client.ping()` and `transport.get_session_id()`
|
||||
|
||||
Conversely, [background tasks](/clients/tasks) are **modern-only**: the tasks capability is negotiated over `2026-07-28` connections, so `mode="legacy"` never triggers one and a task-enabled tool just runs synchronously.
|
||||
|
||||
A FastMCP server serves both eras, so a default client negotiates the modern one and these raise an era-specific error. Pinning the handshake restores them.
|
||||
|
||||
You can also pin a specific modern protocol version to adopt it directly, without a discovery probe:
|
||||
|
||||
```python
|
||||
client = Client("https://example.com/mcp", mode="2026-07-28")
|
||||
```
|
||||
|
||||
Once connected, the negotiated version, server identity, capabilities, and instructions are available as properties. They are populated from either the legacy `InitializeResult` or modern `DiscoverResult`, and reset to `None` when the client disconnects. `instructions` is also `None` when the server does not provide any.
|
||||
|
||||
When you pin a modern version directly, the client skips discovery and adopts that version with minimal synthesized metadata. In that mode, `server_info` has an empty name and `instructions` is `None`.
|
||||
|
||||
```python
|
||||
async with Client("https://example.com/mcp", mode="auto") as client:
|
||||
print(client.protocol_version) # e.g. "2026-07-28"
|
||||
print(client.server_info) # Implementation | None
|
||||
print(client.server_capabilities) # ServerCapabilities | None
|
||||
print(client.instructions) # str | None
|
||||
```
|
||||
|
||||
<Note>
|
||||
`mode="auto"` is the default as of FastMCP 4.0. Earlier versions defaulted to `"legacy"`. If a server behaves unexpectedly under discovery, or you depend on the legacy `initialize` result, pin the old behavior with `Client(..., mode="legacy")`.
|
||||
|
||||
The SSE transport is legacy-only — it cannot carry the sessionless modern era — so a client connecting over SSE always negotiates the legacy handshake, even under `mode="auto"`. A multi-server config (`MCPConfigTransport` with more than one server) is likewise legacy-only, because it mounts each backend behind a legacy-era proxy; a single-server config mirrors its one backend transport's era.
|
||||
</Note>
|
||||
|
||||
## Response caching
|
||||
|
||||
<VersionBadge version="4.0.0" />
|
||||
|
||||
The client can cache the results of `list_tools`, `list_resources`, `list_prompts`, and `read_resource` so that repeated calls avoid a network round-trip. Caching is opt-in and honors the server's own cache hints, so it only takes effect against modern-era servers that advertise them — a cache is inert on a legacy connection.
|
||||
|
||||
Enable the default in-memory cache by passing `cache=True`. It respects the `ttlMs` and `cacheScope` hints the server attaches to each response.
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
client = Client("https://example.com/mcp", mode="auto", cache=True)
|
||||
|
||||
async with client:
|
||||
tools = await client.list_tools() # fetched from the server
|
||||
tools = await client.list_tools() # served from the cache
|
||||
```
|
||||
|
||||
The default (`cache=None`) and `cache=False` both disable caching. For control over the store, TTL, or partitioning, pass a `CacheConfig`. A custom config requires a `target_id`, since in-memory FastMCP transports expose no server URL to derive a shared-store identity from.
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from mcp.client.caching import CacheConfig
|
||||
|
||||
config = CacheConfig(target_id="weather-api", default_ttl_ms=60_000)
|
||||
client = Client("https://example.com/mcp", mode="auto", cache=config)
|
||||
```
|
||||
|
||||
The high-level `list_tools`, `list_resources`, `list_prompts`, and `read_resource` methods always use the cache when one is configured. To override the behavior for a single call, use the lower-level `*_mcp` variants, which accept a `cache_mode` argument: `"use"` (the default) serves and stores, `"refresh"` stores a fresh result without serving a cached one, and `"bypass"` skips the cache entirely.
|
||||
|
||||
```python
|
||||
async with client:
|
||||
fresh = await client.list_tools_mcp(cache_mode="refresh")
|
||||
```
|
||||
|
||||
### Sharing a cache across clients
|
||||
|
||||
The default cache lives in each client's process. To share cached responses across a fleet — a set of proxy replicas backed by one Redis, for example — pass a `KeyValueResponseCacheStore`, FastMCP's adapter over the same `AsyncKeyValue` key-value abstraction the event store and OAuth proxy use. It accepts any compatible backend (memory, Redis, and more).
|
||||
|
||||
A shared store mingles responses from different principals, so it requires an explicit `partition` that isolates them. Derive the partition from a verified credential — never from request data or the server URL — and construct a new client when the principal changes. Only responses the server marks `"public"` are ever served across partitions.
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.caching import KeyValueResponseCacheStore
|
||||
from mcp.client.caching import CacheConfig
|
||||
from key_value.aio.stores.redis import RedisStore
|
||||
|
||||
backend = RedisStore(url="redis://localhost")
|
||||
store = KeyValueResponseCacheStore(storage=backend)
|
||||
|
||||
config = CacheConfig(store=store, partition="tenant-a", target_id="weather-api")
|
||||
client = Client("https://example.com/mcp", mode="auto", cache=config)
|
||||
```
|
||||
|
||||
The adapter serializes each result through a type-tagged envelope validated against an allowlist of cacheable result models, so a value naming an unknown type is treated as a cache miss rather than deserialized blindly. Each store instance owns its own collection namespace; `clear()` affects only that namespace, never another tenant's entries.
|
||||
|
||||
## Client extensions
|
||||
|
||||
<VersionBadge version="4.0.0" />
|
||||
|
||||
Client extensions (SEP-2133) are the advanced mechanism a client uses to opt into vendor capabilities that live outside the core protocol. An extension is a `ClientExtension` instance that bundles three things: a capability *advertisement* the server can read, one or more *result claims* that let the client parse extra `tools/call` result shapes, and *notification bindings* that observe server notifications the core protocol doesn't define. Pass a sequence of them to `extensions=`.
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from myproject.extensions import AppsExtension
|
||||
|
||||
client = Client("https://example.com/mcp", extensions=[AppsExtension()])
|
||||
```
|
||||
|
||||
Each extension's contributions are threaded into the underlying session. FastMCP folds in its own internal extension for [background tasks](/clients/tasks) automatically, and your own extensions *compose* with it rather than replacing it — pass your own tasks extension with the same identifier if you need to override it. When a tool returns a shape an extension claims, `client.call_tool()` resolves it transparently through the owning claim's resolver and hands you back an ordinary result. Result claims and their advertisements are honored only on modern-era connections, so they are inert on a legacy handshake.
|
||||
|
||||
For the rare case where you need to register additional result claims against an extension that is already advertised, pass them through `result_claims=`, keyed by the extension's identifier. Prefer declaring claims on the extension itself; this parameter merges extra claims with an extension's own.
|
||||
|
||||
```python
|
||||
client = Client(
|
||||
"https://example.com/mcp",
|
||||
extensions=[AppsExtension()],
|
||||
result_claims={"example.com/apps": [extra_claim]},
|
||||
)
|
||||
```
|
||||
|
||||
## Operations
|
||||
|
||||
FastMCP clients interact with three types of server components.
|
||||
|
|
@ -201,6 +342,8 @@ See [Prompts](/clients/prompts) for detailed documentation including argument se
|
|||
|
||||
The client supports callback handlers for advanced server interactions. These let you respond to server-initiated requests and receive notifications.
|
||||
|
||||
Sampling, elicitation, and roots are all server-initiated, so they belong to the handshake era described under [protocol negotiation](#protocol-negotiation). A default client negotiates the newest era both peers share, where the server has no back-channel to push those requests down, so an example that exercises them pins `mode="legacy"`. Logging and progress arrive as notifications on the response stream and work in either era.
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.logging import LogMessage
|
||||
|
|
@ -217,6 +360,7 @@ async def sampling_handler(messages, params, context):
|
|||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
mode="legacy",
|
||||
log_handler=log_handler,
|
||||
progress_handler=progress_handler,
|
||||
sampling_handler=sampling_handler,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ Use this when you need to respond to server requests for user input during tool
|
|||
|
||||
Elicitation allows MCP servers to request structured input from users during operations. Instead of requiring all inputs upfront, servers can interactively ask for missing parameters, request clarification, or gather additional context.
|
||||
|
||||
Two routes reach that outcome, and the protocol version the client negotiates decides which one applies. On older versions the server pushes an elicitation request down to the client, over the connection the `initialize` handshake opens; that is the flow the next few sections describe. On `2026-07-28` and later the server instead returns a description of what it needs, and the client answers with a fresh call — see [input-required rounds](#input-required-rounds). You write the same `elicitation_handler` either way — FastMCP routes it to whichever mechanism the connection supports.
|
||||
|
||||
<Note>
|
||||
**This page shows the older protocol's elicitation flow.** On protocol version `2026-07-28` the server instead returns a description of what it needs and the client answers with a new call — see [input-required rounds](#input-required-rounds). The same `elicitation_handler` serves both. Clients default to `mode="auto"`, so the examples below pass `mode="legacy"` to exercise the server-initiated flow. See [protocol negotiation](/clients/client#protocol-negotiation).
|
||||
</Note>
|
||||
|
||||
## Handler Template
|
||||
|
||||
```python
|
||||
|
|
@ -30,8 +36,8 @@ async def elicitation_handler(
|
|||
|
||||
Args:
|
||||
message: The prompt to display to the user
|
||||
response_type: Python dataclass type for the response (None if no data expected)
|
||||
params: Original MCP elicitation parameters including raw JSON schema
|
||||
response_type: Python dataclass type for form responses (None for URL requests or empty schemas)
|
||||
params: Original MCP elicitation parameters
|
||||
context: Request context with metadata
|
||||
|
||||
Returns:
|
||||
|
|
@ -44,18 +50,24 @@ async def elicitation_handler(
|
|||
if not user_input:
|
||||
return ElicitResult(action="decline")
|
||||
|
||||
# URL requests and empty-object schemas have no response type to construct,
|
||||
# so accepting is the whole response.
|
||||
if response_type is None:
|
||||
return ElicitResult(action="accept")
|
||||
|
||||
# Create response using the provided dataclass type
|
||||
return response_type(value=user_input)
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
mode="legacy",
|
||||
elicitation_handler=elicitation_handler,
|
||||
)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
When a server needs user input, it sends an elicitation request with a message prompt and a JSON schema describing the expected response structure. FastMCP automatically converts this schema into a Python dataclass type, making it easy to construct properly typed responses without manually parsing JSON schemas.
|
||||
When a server needs user input, it sends an elicitation request with a message prompt. Form elicitation requests include a JSON schema describing the expected response structure, and FastMCP automatically converts that schema into a Python dataclass type. URL elicitation requests and empty-object schemas use `response_type=None`.
|
||||
|
||||
The handler receives four parameters:
|
||||
|
||||
|
|
@ -65,11 +77,11 @@ The handler receives four parameters:
|
|||
</ResponseField>
|
||||
|
||||
<ResponseField name="response_type" type="type | None">
|
||||
A Python dataclass type that FastMCP created from the server's JSON schema. Use this to construct your response with proper typing. If the server requests an empty object, this will be `None`.
|
||||
A Python dataclass type that FastMCP created from a form request's JSON schema. Use this to construct your response with proper typing. For URL requests or empty-object schemas, this will be `None`.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="params" type="ElicitRequestParams">
|
||||
The original MCP elicitation parameters, including the raw JSON schema in `params.requestedSchema`
|
||||
The original MCP elicitation parameters. Form requests carry the raw JSON schema on `params.requested_schema`; URL requests carry `params.url` instead and have no schema.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="context" type="RequestContext">
|
||||
|
|
@ -133,6 +145,24 @@ async def elicitation_handler(message, response_type, params, context):
|
|||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
mode="legacy",
|
||||
elicitation_handler=elicitation_handler
|
||||
)
|
||||
```
|
||||
|
||||
## Input-required rounds
|
||||
|
||||
<VersionBadge version="4.0.0" />
|
||||
|
||||
On protocol version `2026-07-28` and later, a server can ask for input before it returns a final result. Nothing is held open: the tool *returns* a description of what it needs, which completes that round as an ordinary response, and the client answers by issuing a **new** `call_tool`, `get_prompt`, or `read_resource` request carrying the answer. `fastmcp.Client` drives that loop for you — it fulfils each round's requests using the callbacks you already configured (your `elicitation_handler`, `sampling_handler`, and roots) and repeats until the call reaches a terminal result. No extra wiring is needed beyond the handlers described above.
|
||||
|
||||
The `input_required_max_rounds` parameter caps how many rounds the client will answer before giving up, guarding against a server that never terminates. It defaults to `10`.
|
||||
|
||||
```python
|
||||
client = Client(
|
||||
"https://example.com/mcp",
|
||||
mode="auto",
|
||||
elicitation_handler=elicitation_handler,
|
||||
input_required_max_rounds=5,
|
||||
)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ Pass the full MCP endpoint URL for the remote server. Many FastMCP HTTP servers
|
|||
|
||||
`fastmcp-remote` starts a local stdio bridge, then connects to the upstream server when the MCP host initializes that bridge. If the upstream server is unavailable, the URL does not point to an MCP endpoint, or authentication cannot complete, initialization fails and the host should report the remote server as failed. After initialization succeeds, later tool, resource, prompt, and ping requests continue to proxy through the same remote server configuration.
|
||||
|
||||
OAuth is enabled automatically for HTTPS servers. The first connection opens the browser-based OAuth flow when the server requires authentication, then stores tokens locally for future runs.
|
||||
OAuth is enabled automatically unless you provide an `Authorization` header or pass `--auth none`. The first connection opens the browser-based OAuth flow when the server requires authentication, then stores tokens locally for future runs.
|
||||
|
||||
To pass a bearer token or another custom header directly, provide `--header` in `Name: Value` form. The header name ends at the first colon, so values can contain additional colons. Quote the header when the value contains spaces, just like any other shell argument. An `Authorization` header disables OAuth by default:
|
||||
|
||||
|
|
|
|||
|
|
@ -32,8 +32,19 @@ LOGGING_LEVEL_MAP = logging.getLevelNamesMapping()
|
|||
|
||||
async def log_handler(message: LogMessage):
|
||||
"""Forward MCP server logs to Python's logging system."""
|
||||
msg = message.data.get('msg')
|
||||
extra = message.data.get('extra')
|
||||
data = message.data
|
||||
if isinstance(data, dict):
|
||||
msg = data.get('msg', data)
|
||||
extra = data.get('extra')
|
||||
else:
|
||||
msg = data
|
||||
extra = None
|
||||
|
||||
# Python's logging requires `extra` to be a mapping, but a server can send
|
||||
# any JSON value, so fold anything else into the message instead.
|
||||
if extra is not None and not isinstance(extra, dict):
|
||||
msg = f"{msg} ({extra})"
|
||||
extra = None
|
||||
|
||||
level = LOGGING_LEVEL_MAP.get(message.level.upper(), logging.INFO)
|
||||
logger.log(level, msg, extra=extra)
|
||||
|
|
@ -55,19 +66,20 @@ The handler receives a `LogMessage` object:
|
|||
The logger name (may be None)
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="data" type="dict">
|
||||
The log payload, containing `msg` and `extra` keys
|
||||
<ResponseField name="data" type="Any">
|
||||
The JSON-serializable log payload sent by the server. FastMCP's structured logger uses a dictionary with `msg` and `extra` keys, but other MCP servers may send any JSON value.
|
||||
</ResponseField>
|
||||
</Card>
|
||||
|
||||
## Structured Logs
|
||||
|
||||
The `message.data` attribute is a dictionary containing the log payload. This enables structured logging with rich contextual information.
|
||||
The `message.data` attribute contains the server's JSON-serializable log payload. FastMCP servers commonly send a dictionary with `msg` and `extra` keys, which enables structured logging with rich contextual information.
|
||||
|
||||
```python
|
||||
async def detailed_log_handler(message: LogMessage):
|
||||
msg = message.data.get('msg')
|
||||
extra = message.data.get('extra')
|
||||
data = message.data
|
||||
msg = data.get('msg', data) if isinstance(data, dict) else data
|
||||
extra = data.get('extra') if isinstance(data, dict) else None
|
||||
|
||||
if message.level == "error":
|
||||
print(f"ERROR: {msg} | Details: {extra}")
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ from fastmcp import Client
|
|||
|
||||
async def message_handler(message):
|
||||
"""Handle MCP notifications from the server."""
|
||||
if hasattr(message, 'root'):
|
||||
method = message.root.method
|
||||
if hasattr(message, 'method'):
|
||||
method = message.method
|
||||
|
||||
if method == "notifications/tools/list_changed":
|
||||
print("Tools have changed - refresh tool cache")
|
||||
|
|
@ -31,6 +31,8 @@ async def message_handler(message):
|
|||
print("Resources have changed")
|
||||
elif method == "notifications/prompts/list_changed":
|
||||
print("Prompts have changed")
|
||||
elif method == "notifications/resources/updated":
|
||||
print("A resource was updated")
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
|
|
@ -45,23 +47,23 @@ For fine-grained targeting, subclass `MessageHandler` to use specific hooks:
|
|||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.messages import MessageHandler
|
||||
import mcp.types
|
||||
import mcp_types
|
||||
|
||||
class MyMessageHandler(MessageHandler):
|
||||
async def on_tool_list_changed(
|
||||
self, notification: mcp.types.ToolListChangedNotification
|
||||
self, notification: mcp_types.ToolListChangedNotification
|
||||
) -> None:
|
||||
"""Handle tool list changes."""
|
||||
print("Tool list changed - refreshing available tools")
|
||||
|
||||
async def on_resource_list_changed(
|
||||
self, notification: mcp.types.ResourceListChangedNotification
|
||||
self, notification: mcp_types.ResourceListChangedNotification
|
||||
) -> None:
|
||||
"""Handle resource list changes."""
|
||||
print("Resource list changed")
|
||||
|
||||
async def on_prompt_list_changed(
|
||||
self, notification: mcp.types.PromptListChangedNotification
|
||||
self, notification: mcp_types.PromptListChangedNotification
|
||||
) -> None:
|
||||
"""Handle prompt list changes."""
|
||||
print("Prompt list changed")
|
||||
|
|
@ -76,7 +78,7 @@ client = Client(
|
|||
|
||||
```python
|
||||
from fastmcp.client.messages import MessageHandler
|
||||
import mcp.types
|
||||
import mcp_types
|
||||
|
||||
class MyMessageHandler(MessageHandler):
|
||||
async def on_message(self, message) -> None:
|
||||
|
|
@ -84,37 +86,49 @@ class MyMessageHandler(MessageHandler):
|
|||
pass
|
||||
|
||||
async def on_notification(
|
||||
self, notification: mcp.types.ServerNotification
|
||||
self, notification: mcp_types.ServerNotification
|
||||
) -> None:
|
||||
"""Called for notifications (fire-and-forget)."""
|
||||
pass
|
||||
|
||||
async def on_tool_list_changed(
|
||||
self, notification: mcp.types.ToolListChangedNotification
|
||||
self, notification: mcp_types.ToolListChangedNotification
|
||||
) -> None:
|
||||
"""Called when the server's tool list changes."""
|
||||
pass
|
||||
|
||||
async def on_resource_list_changed(
|
||||
self, notification: mcp.types.ResourceListChangedNotification
|
||||
self, notification: mcp_types.ResourceListChangedNotification
|
||||
) -> None:
|
||||
"""Called when the server's resource list changes."""
|
||||
pass
|
||||
|
||||
async def on_prompt_list_changed(
|
||||
self, notification: mcp.types.PromptListChangedNotification
|
||||
self, notification: mcp_types.PromptListChangedNotification
|
||||
) -> None:
|
||||
"""Called when the server's prompt list changes."""
|
||||
pass
|
||||
|
||||
async def on_progress(
|
||||
self, notification: mcp.types.ProgressNotification
|
||||
self, notification: mcp_types.ProgressNotification
|
||||
) -> None:
|
||||
"""Called for progress updates during long-running operations."""
|
||||
pass
|
||||
|
||||
async def on_resource_updated(
|
||||
self, notification: mcp_types.ResourceUpdatedNotification
|
||||
) -> None:
|
||||
"""Called when a specific resource changes."""
|
||||
pass
|
||||
|
||||
async def on_cancelled(
|
||||
self, notification: mcp_types.CancelledNotification
|
||||
) -> None:
|
||||
"""Called when a request is cancelled."""
|
||||
pass
|
||||
|
||||
async def on_logging_message(
|
||||
self, notification: mcp.types.LoggingMessageNotification
|
||||
self, notification: mcp_types.LoggingMessageNotification
|
||||
) -> None:
|
||||
"""Called for log messages from the server."""
|
||||
pass
|
||||
|
|
@ -127,14 +141,14 @@ A practical example of maintaining a tool cache that refreshes when tools change
|
|||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.messages import MessageHandler
|
||||
import mcp.types
|
||||
import mcp_types
|
||||
|
||||
class ToolCacheHandler(MessageHandler):
|
||||
def __init__(self):
|
||||
self.cached_tools = []
|
||||
|
||||
async def on_tool_list_changed(
|
||||
self, notification: mcp.types.ToolListChangedNotification
|
||||
self, notification: mcp_types.ToolListChangedNotification
|
||||
) -> None:
|
||||
"""Clear tool cache when tools change."""
|
||||
print("Tools changed - clearing cache")
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Request a rendered prompt with `get_prompt()`:
|
|||
async with client:
|
||||
# Simple prompt without arguments
|
||||
result = await client.get_prompt("welcome_message")
|
||||
# result -> mcp.types.GetPromptResult
|
||||
# result -> mcp_types.GetPromptResult
|
||||
|
||||
# Access the generated messages
|
||||
for message in result.messages:
|
||||
|
|
@ -128,12 +128,12 @@ See [Metadata](/servers/versioning#version-discovery) for how to discover availa
|
|||
|
||||
## Multi-Server Clients
|
||||
|
||||
When using multi-server clients, prompts are accessible directly without prefixing:
|
||||
When using multi-server clients, prompts are mounted with the server name as a prefix, just like tools:
|
||||
|
||||
```python
|
||||
async with client: # Multi-server client
|
||||
result1 = await client.get_prompt("weather_prompt", {"city": "London"})
|
||||
result2 = await client.get_prompt("assistant_prompt", {"query": "help"})
|
||||
result1 = await client.get_prompt("weather_weather_prompt", {"city": "London"})
|
||||
result2 = await client.get_prompt("assistant_assistant_prompt", {"query": "help"})
|
||||
```
|
||||
|
||||
## Raw Protocol Access
|
||||
|
|
@ -143,5 +143,5 @@ For complete control, use `get_prompt_mcp()` which returns the full MCP protocol
|
|||
```python
|
||||
async with client:
|
||||
result = await client.get_prompt_mcp("example_prompt", {"arg": "value"})
|
||||
# result -> mcp.types.GetPromptResult
|
||||
# result -> mcp_types.GetPromptResult
|
||||
```
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ async with client:
|
|||
for item in content:
|
||||
if hasattr(item, 'text'):
|
||||
print(f"Text content: {item.text}")
|
||||
print(f"MIME type: {item.mimeType}")
|
||||
print(f"MIME type: {item.mime_type}")
|
||||
```
|
||||
|
||||
Binary resources include images, PDFs, and other non-text data:
|
||||
|
|
@ -65,7 +65,7 @@ async with client:
|
|||
for item in content:
|
||||
if hasattr(item, 'blob'):
|
||||
print(f"Binary content: {len(item.blob)} bytes")
|
||||
print(f"MIME type: {item.mimeType}")
|
||||
print(f"MIME type: {item.mime_type}")
|
||||
|
||||
# Save to file
|
||||
with open("downloaded_logo.png", "wb") as f:
|
||||
|
|
@ -106,5 +106,5 @@ For complete control, use `read_resource_mcp()` which returns the full MCP proto
|
|||
```python
|
||||
async with client:
|
||||
result = await client.read_resource_mcp("resource://example")
|
||||
# result -> mcp.types.ReadResourceResult
|
||||
# result -> mcp_types.ReadResourceResult
|
||||
```
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ Use this when you need to tell servers what local resources the client has acces
|
|||
|
||||
Roots inform servers about resources the client can provide. Servers can use this information to adjust behavior or provide more relevant responses.
|
||||
|
||||
<Note>
|
||||
**Roots require the older MCP protocol.** A server reads roots by sending a request down to the client, and protocol version `2026-07-28` removed the server's ability to do that. Clients default to `mode="auto"`, which negotiates the newest version both sides support, so the examples below pass `mode="legacy"`. See [protocol negotiation](/clients/client#protocol-negotiation).
|
||||
</Note>
|
||||
|
||||
## Static Roots
|
||||
|
||||
Provide a list of roots when creating the client:
|
||||
|
|
@ -22,6 +26,7 @@ from fastmcp import Client
|
|||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
mode="legacy",
|
||||
roots=["/path/to/root1", "/path/to/root2"]
|
||||
)
|
||||
```
|
||||
|
|
@ -40,6 +45,7 @@ async def roots_callback(context: RequestContext) -> list[str]:
|
|||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
mode="legacy",
|
||||
roots=roots_callback
|
||||
)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ Use this when you need to respond to server requests for LLM completions.
|
|||
|
||||
MCP servers can request LLM completions from clients during tool execution. This enables servers to delegate AI reasoning to the client, which controls which LLM is used and how requests are made.
|
||||
|
||||
<Note>
|
||||
**Sampling requires the older MCP protocol.** A server requests sampling by sending a request down to the client, and protocol version `2026-07-28` removed the server's ability to do that. Clients default to `mode="auto"`, which negotiates the newest version both sides support, so every example on this page passes `mode="legacy"`. See [protocol negotiation](/clients/client#protocol-negotiation).
|
||||
</Note>
|
||||
|
||||
## Handler Template
|
||||
|
||||
```python
|
||||
|
|
@ -42,13 +46,14 @@ async def sampling_handler(
|
|||
conversation.append(f"{message.role}: {content}")
|
||||
|
||||
# Use the system prompt if provided
|
||||
system_prompt = params.systemPrompt or "You are a helpful assistant."
|
||||
system_prompt = params.system_prompt or "You are a helpful assistant."
|
||||
|
||||
# Integrate with your LLM service here
|
||||
return "Generated response based on the messages"
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
mode="legacy",
|
||||
sampling_handler=sampling_handler,
|
||||
)
|
||||
```
|
||||
|
|
@ -109,6 +114,7 @@ from fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler
|
|||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
mode="legacy",
|
||||
sampling_handler=OpenAISamplingHandler(default_model="gpt-4o"),
|
||||
)
|
||||
```
|
||||
|
|
@ -120,6 +126,7 @@ from openai import AsyncOpenAI
|
|||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
mode="legacy",
|
||||
sampling_handler=OpenAISamplingHandler(
|
||||
default_model="llama-3.1-70b",
|
||||
client=AsyncOpenAI(base_url="http://localhost:8000/v1"),
|
||||
|
|
@ -128,7 +135,7 @@ client = Client(
|
|||
```
|
||||
|
||||
<Note>
|
||||
Install the OpenAI handler with `pip install fastmcp[openai]`.
|
||||
Install the OpenAI handler with `pip install 'fastmcp[openai]'`.
|
||||
</Note>
|
||||
|
||||
### Anthropic Handler
|
||||
|
|
@ -141,12 +148,13 @@ from fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler
|
|||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
mode="legacy",
|
||||
sampling_handler=AnthropicSamplingHandler(default_model="claude-sonnet-4-5"),
|
||||
)
|
||||
```
|
||||
|
||||
<Note>
|
||||
Install the Anthropic handler with `pip install fastmcp[anthropic]`.
|
||||
Install the Anthropic handler with `pip install 'fastmcp[anthropic]'`.
|
||||
</Note>
|
||||
|
||||
### Google Gemini Handler
|
||||
|
|
@ -159,12 +167,13 @@ from fastmcp.client.sampling.handlers.google_genai import GoogleGenaiSamplingHan
|
|||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
mode="legacy",
|
||||
sampling_handler=GoogleGenaiSamplingHandler(default_model="gemini-2.0-flash"),
|
||||
)
|
||||
```
|
||||
|
||||
<Note>
|
||||
Install the Google Gemini handler with `pip install fastmcp[gemini]`.
|
||||
Install the Google Gemini handler with `pip install 'fastmcp[gemini]'`.
|
||||
</Note>
|
||||
|
||||
## Sampling Capabilities
|
||||
|
|
@ -172,10 +181,11 @@ Install the Google Gemini handler with `pip install fastmcp[gemini]`.
|
|||
When you provide a `sampling_handler`, FastMCP automatically advertises full sampling capabilities to the server, including tool support. To disable tool support for simpler handlers:
|
||||
|
||||
```python
|
||||
from mcp.types import SamplingCapability
|
||||
from mcp_types import SamplingCapability
|
||||
|
||||
client = Client(
|
||||
"my_mcp_server.py",
|
||||
mode="legacy",
|
||||
sampling_handler=basic_handler,
|
||||
sampling_capabilities=SamplingCapability(), # No tool support
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,180 +1,138 @@
|
|||
---
|
||||
title: Background Tasks
|
||||
sidebarTitle: Tasks
|
||||
description: Execute operations asynchronously and track their progress.
|
||||
description: Call long-running tools without blocking, and answer questions they ask mid-run.
|
||||
icon: clock
|
||||
tag: "NEW"
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="2.14.0" />
|
||||
<VersionBadge version="4.0.0" />
|
||||
|
||||
Use this when you need to run long operations asynchronously while doing other work.
|
||||
Some tool calls take a while. The MCP background tasks extension lets a server run one in the background instead of holding the request open, and FastMCP's client drives the whole thing for you — most of the time you don't need to know a call was tasked at all.
|
||||
|
||||
The MCP task protocol lets you request operations to run in the background. The call returns a Task object immediately, letting you track progress, cancel operations, or await results.
|
||||
<Note>
|
||||
**Client task support is opt-in.** Install the `fastmcp-tasks` package (`pip install "fastmcp[tasks]"`) and import it — importing `fastmcp_tasks` anywhere (which you do to use `call_tool_task`) enables task support for every `Client` in the process. Without it, a `Client` never advertises the tasks capability, so the server runs its calls synchronously and background tasks simply don't happen.
|
||||
|
||||
## Requesting Background Execution
|
||||
**Tasks also require the modern protocol.** The capability is negotiated over `2026-07-28` connections. `mode="auto"` (the client default) negotiates it automatically; `mode="legacy"` never does. See [protocol negotiation](/clients/client#protocol-negotiation).
|
||||
</Note>
|
||||
|
||||
Pass `task=True` to run an operation as a background task:
|
||||
## Transparent Calls
|
||||
|
||||
With task support enabled, just call the tool. If the server runs it as a background task, `call_tool` polls it to completion under the hood and returns the same result you'd get from a synchronous call — the task is invisible.
|
||||
|
||||
```python
|
||||
import fastmcp_tasks # enables client task support
|
||||
from fastmcp import Client
|
||||
|
||||
async with Client(server, mode="auto") as client:
|
||||
result = await client.call_tool("slow_computation", {"duration": 10})
|
||||
print(result.data)
|
||||
```
|
||||
|
||||
This is the right default for most code: it works whether or not the server actually tasks the call, so you can write ordinary tool-calling code without checking server capabilities.
|
||||
|
||||
## Driving a Task Explicitly
|
||||
|
||||
When you want to do other work while a task runs — or check on it, or cancel it — use `call_tool_task` instead. It returns a `ToolTask` handle immediately rather than waiting for completion.
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp_tasks import call_tool_task
|
||||
|
||||
async with Client(server) as client:
|
||||
# Start a background task
|
||||
task = await client.call_tool("slow_computation", {"duration": 10}, task=True)
|
||||
|
||||
async with Client(server, mode="auto") as client:
|
||||
task = await call_tool_task(client, "slow_computation", {"duration": 10})
|
||||
print(f"Task started: {task.task_id}")
|
||||
|
||||
# Do other work while it runs...
|
||||
|
||||
# Get the result when ready
|
||||
result = await task.result()
|
||||
```
|
||||
|
||||
This works with tools, resources, and prompts:
|
||||
|
||||
```python
|
||||
tool_task = await client.call_tool("my_tool", args, task=True)
|
||||
resource_task = await client.read_resource("file://large.txt", task=True)
|
||||
prompt_task = await client.get_prompt("my_prompt", args, task=True)
|
||||
```
|
||||
|
||||
## Task API
|
||||
|
||||
All task types share a common interface.
|
||||
|
||||
### Getting Results
|
||||
|
||||
Call `await task.result()` or simply `await task` to block until the task completes:
|
||||
|
||||
```python
|
||||
task = await client.call_tool("analyze", {"text": "hello"}, task=True)
|
||||
|
||||
# Wait for result (blocking)
|
||||
result = await task.result()
|
||||
# or: result = await task
|
||||
```
|
||||
`call_tool_task` requires the server to actually run the call as a task — if the tool isn't `task=True`, or the server doesn't have the tasks extension registered, it raises `ToolError`. Use it when you specifically need the handle; use `call_tool` when you just want the result.
|
||||
|
||||
### Checking Status
|
||||
|
||||
Check the current status without blocking:
|
||||
|
||||
```python
|
||||
status = await task.status()
|
||||
print(f"{status.status}: {status.statusMessage}")
|
||||
# status.status is "working", "completed", "failed", or "cancelled"
|
||||
print(f"{status.status}: {status.status_message}")
|
||||
# status.status is "working", "input_required", "completed", "failed", or "cancelled"
|
||||
```
|
||||
|
||||
### Waiting with Control
|
||||
|
||||
Use `task.wait()` for more control over waiting:
|
||||
`task.wait()` polls until a terminal state (or a specific one you name), without answering any input the task asks for — use it when you want to observe an `input_required` pause yourself rather than have it answered automatically.
|
||||
|
||||
```python
|
||||
# Wait up to 30 seconds for completion
|
||||
status = await task.wait(timeout=30.0)
|
||||
|
||||
# Wait for a specific state
|
||||
status = await task.wait(state="completed", timeout=30.0)
|
||||
status = await task.wait(state="input_required", timeout=30.0)
|
||||
```
|
||||
|
||||
### Cancellation
|
||||
### Getting the Result
|
||||
|
||||
Cancel a running task:
|
||||
`task.result()` drives the task the rest of the way — including answering any input it asks for — and returns the finished result, same as `client.call_tool` would. Awaiting the task directly is shorthand for this.
|
||||
|
||||
```python
|
||||
result = await task.result()
|
||||
# or: result = await task
|
||||
```
|
||||
|
||||
By default a failed or cancelled task raises `ToolError`. Pass `raise_on_error=False` to `call_tool_task` to get an error result back instead.
|
||||
|
||||
### Cancellation
|
||||
|
||||
```python
|
||||
await task.cancel()
|
||||
```
|
||||
|
||||
## Status Updates
|
||||
Cancellation is cooperative — the task may still finish before the server notices the request.
|
||||
|
||||
Register callbacks to receive real-time status updates as the server reports progress:
|
||||
## Answering Questions Mid-Task
|
||||
|
||||
```python
|
||||
def on_status_change(status):
|
||||
print(f"Task {status.taskId}: {status.status} - {status.statusMessage}")
|
||||
|
||||
task.on_status_change(on_status_change)
|
||||
|
||||
# Async callbacks work too
|
||||
async def on_status_async(status):
|
||||
await log_status(status)
|
||||
|
||||
task.on_status_change(on_status_async)
|
||||
```
|
||||
|
||||
### Handler Template
|
||||
A task can pause partway through to ask a question, the same way a foreground [multi-round-trip](/clients/elicitation#input-required-rounds) tool does. Pass an `elicitation_handler` and both `call_tool` and `task.result()` answer it automatically as part of driving the task to completion:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
def status_handler(status):
|
||||
"""
|
||||
Handle task status updates.
|
||||
async def handle_elicitation(message, response_type, params, context):
|
||||
return {"cuisine": "Thai", "vegetarian": True}
|
||||
|
||||
Args:
|
||||
status: Task status object with:
|
||||
- taskId: Unique task identifier
|
||||
- status: "working", "completed", "failed", or "cancelled"
|
||||
- statusMessage: Optional progress message from server
|
||||
"""
|
||||
if status.status == "working":
|
||||
print(f"Progress: {status.statusMessage}")
|
||||
elif status.status == "completed":
|
||||
print("Task completed")
|
||||
elif status.status == "failed":
|
||||
print(f"Task failed: {status.statusMessage}")
|
||||
|
||||
task.on_status_change(status_handler)
|
||||
async with Client(server, mode="auto", elicitation_handler=handle_elicitation) as client:
|
||||
result = await client.call_tool("plan_dinner", {})
|
||||
print(result.data)
|
||||
```
|
||||
|
||||
## Graceful Degradation
|
||||
|
||||
You can always pass `task=True` regardless of whether the server supports background tasks. Per the MCP specification, servers without task support execute the operation immediately and return the result inline.
|
||||
|
||||
```python
|
||||
task = await client.call_tool("my_tool", args, task=True)
|
||||
|
||||
if task.returned_immediately:
|
||||
print("Server executed immediately (no background support)")
|
||||
else:
|
||||
print("Running in background")
|
||||
|
||||
# Either way, this works
|
||||
result = await task.result()
|
||||
```
|
||||
|
||||
This lets you write task-aware client code without worrying about server capabilities.
|
||||
Without an `elicitation_handler`, a task that asks for input raises `ToolError` rather than hanging. See [server-side background tasks](/servers/tasks#gathering-input-mid-task) for how a tool asks a question in the first place.
|
||||
|
||||
## Example
|
||||
|
||||
Putting it together, here is a client that submits a background task with `call_tool_task` and awaits its result:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from fastmcp import Client
|
||||
from fastmcp_tasks import call_tool_task
|
||||
|
||||
async def main():
|
||||
async with Client(server) as client:
|
||||
# Start background task
|
||||
task = await client.call_tool(
|
||||
"slow_computation",
|
||||
{"duration": 10},
|
||||
task=True,
|
||||
)
|
||||
async with Client(server, mode="auto") as client:
|
||||
# Return immediately and drive the task yourself
|
||||
task = await call_tool_task(client, "slow_computation", {"duration": 10})
|
||||
print(f"Task started: {task.task_id}")
|
||||
|
||||
# Subscribe to updates
|
||||
def on_update(status):
|
||||
print(f"Progress: {status.statusMessage}")
|
||||
# Do other work while the task runs
|
||||
while True:
|
||||
status = await task.status()
|
||||
if status.status in ("completed", "failed", "cancelled"):
|
||||
break
|
||||
print(f"Still working... ({status.status})")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
task.on_status_change(on_update)
|
||||
|
||||
# Do other work while task runs
|
||||
print("Doing other work...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Wait for completion and get result
|
||||
result = await task.result()
|
||||
print(f"Result: {result.content}")
|
||||
print(f"Result: {result.data}")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ async with client:
|
|||
Fully hydrated Python objects with complex type support (datetimes, UUIDs, custom classes). FastMCP exclusive.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name=".content" type="list[mcp.types.ContentBlock]">
|
||||
<ResponseField name=".content" type="list[mcp_types.ContentBlock]">
|
||||
Standard MCP content blocks (`TextContent`, `ImageContent`, `AudioContent`, etc.).
|
||||
</ResponseField>
|
||||
|
||||
|
|
@ -173,9 +173,9 @@ For complete control, use `call_tool_mcp()` which returns the raw MCP protocol o
|
|||
```python
|
||||
async with client:
|
||||
result = await client.call_tool_mcp("my_tool", {"param": "value"})
|
||||
# result -> mcp.types.CallToolResult
|
||||
# result -> mcp_types.CallToolResult
|
||||
|
||||
if result.isError:
|
||||
if result.is_error:
|
||||
print(f"Tool failed: {result.content}")
|
||||
else:
|
||||
print(f"Tool succeeded: {result.content}")
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ Transports handle the underlying connection between your client and MCP servers.
|
|||
STDIO transport communicates with MCP servers through subprocess pipes. When using STDIO, your client launches and manages the server process, controlling its lifecycle and environment.
|
||||
|
||||
<Warning>
|
||||
STDIO servers run in isolated environments by default. They do not inherit your shell's environment variables. You must explicitly pass any configuration the server needs.
|
||||
STDIO servers inherit only a small allowlist of environment variables — just enough to locate an interpreter and a home directory. Anything else in your shell, including API keys and other credentials, does not reach the server unless you pass it through `env` explicitly.
|
||||
|
||||
The allowlist is platform-specific. On POSIX systems it is `HOME`, `LOGNAME`, `PATH`, `SHELL`, `TERM`, and `USER`; on Windows it is `APPDATA`, `HOMEDRIVE`, `HOMEPATH`, `LOCALAPPDATA`, `PATH`, `PATHEXT`, `PROCESSOR_ARCHITECTURE`, `SYSTEMDRIVE`, `SYSTEMROOT`, `TEMP`, `USERNAME`, and `USERPROFILE`.
|
||||
</Warning>
|
||||
|
||||
```python
|
||||
|
|
@ -42,7 +44,7 @@ client = Client("my_server.py") # Limited - no configuration options
|
|||
|
||||
### Environment Variables
|
||||
|
||||
Since STDIO servers do not inherit your environment, you need strategies for passing configuration.
|
||||
Values you pass through `env` are merged on top of the inherited allowlist, so you add configuration rather than replacing the base environment. Anything your server needs beyond those six variables has to be listed explicitly.
|
||||
|
||||
**Selective forwarding** passes only the variables your server needs:
|
||||
|
||||
|
|
@ -63,7 +65,11 @@ client = Client(transport)
|
|||
from dotenv import dotenv_values
|
||||
from fastmcp.client.transports import StdioTransport
|
||||
|
||||
env = dotenv_values(".env")
|
||||
env = {
|
||||
key: value
|
||||
for key, value in dotenv_values(".env").items()
|
||||
if value is not None
|
||||
}
|
||||
transport = StdioTransport(command="python", args=["server.py"], env=env)
|
||||
client = Client(transport)
|
||||
```
|
||||
|
|
@ -80,7 +86,7 @@ client = Client(transport)
|
|||
|
||||
async def efficient_multiple_operations():
|
||||
async with client:
|
||||
await client.ping()
|
||||
await client.list_tools()
|
||||
|
||||
async with client: # Reuses the same subprocess
|
||||
await client.call_tool("process_data", {"file": "data.csv"})
|
||||
|
|
@ -126,7 +132,7 @@ client = Client(
|
|||
|
||||
### SSL Verification
|
||||
|
||||
By default, HTTPS connections verify the server's SSL certificate. You can customize this behavior with the `verify` parameter, which accepts the same values as [httpx](https://www.python-httpx.org/advanced/ssl/):
|
||||
By default, HTTPS connections verify the server's SSL certificate. You can customize this behavior with the `verify` parameter, which accepts the same values as httpx2 (documented in [httpx's SSL guide](https://www.python-httpx.org/advanced/ssl/), which httpx2 follows):
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
|
|
|||
|
|
@ -57,6 +57,42 @@ h6 code:not(pre code) {
|
|||
background: linear-gradient(135deg, #2d00f7 0%, #4cc9f0 100%);
|
||||
}
|
||||
|
||||
/* V3 banner - inside content-container, breaks out of padding with negative margins */
|
||||
#v3-banner {
|
||||
display: block;
|
||||
background: linear-gradient(135deg, #4cc9f0 0%, #2d00f7 100%);
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 10px 16px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin: -2rem -2rem 1.5rem -2rem;
|
||||
width: calc(100% + 4rem);
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
#v3-banner a {
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
#v3-banner a:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
#v3-banner {
|
||||
margin: -3rem -4rem 1.5rem -4rem;
|
||||
width: calc(100% + 8rem);
|
||||
}
|
||||
}
|
||||
|
||||
.dark #v3-banner {
|
||||
background: linear-gradient(135deg, #2d00f7 0%, #4cc9f0 100%);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -79,17 +79,17 @@ The ASGI approach shines in production environments where you need reliability a
|
|||
|
||||
### Custom Path
|
||||
|
||||
By default, your MCP server is accessible at `/mcp/` on your domain. You can customize this path to fit your URL structure or avoid conflicts with existing endpoints. This is particularly useful when integrating MCP into an existing application or following specific API conventions.
|
||||
By default, your MCP server is accessible at `/mcp` on your domain. You can customize this path to fit your URL structure or avoid conflicts with existing endpoints. This is particularly useful when integrating MCP into an existing application or following specific API conventions.
|
||||
|
||||
```python
|
||||
# Option 1: With mcp.run()
|
||||
mcp.run(transport="http", host="0.0.0.0", port=8000, path="/api/mcp/")
|
||||
mcp.run(transport="http", host="0.0.0.0", port=8000, path="/api/mcp")
|
||||
|
||||
# Option 2: With ASGI app
|
||||
app = mcp.http_app(path="/api/mcp/")
|
||||
app = mcp.http_app(path="/api/mcp")
|
||||
```
|
||||
|
||||
Now your server is accessible at `http://localhost:8000/api/mcp/`.
|
||||
Now your server is accessible at `http://localhost:8000/api/mcp`.
|
||||
|
||||
### Authentication
|
||||
|
||||
|
|
@ -103,11 +103,11 @@ If you're mounting an authenticated server under a path prefix, see [Mounting Au
|
|||
|
||||
### Host and Origin Protection
|
||||
|
||||
FastMCP validates `Host` and browser `Origin` headers for Streamable HTTP requests by default. This protects localhost-bound servers from DNS rebinding attacks and rejects browser requests from origins you have not trusted.
|
||||
FastMCP can validate `Host` and browser `Origin` headers for Streamable HTTP requests before they reach MCP session handling. This request guard protects localhost-bound servers from DNS rebinding attacks, and it remains opt-in in FastMCP 3.x to preserve compatibility with existing ASGI, serverless, and reverse-proxy deployments.
|
||||
|
||||
Think of this as a request guard rather than CORS middleware. It decides whether a request can reach MCP session handling. CORS remains a separate browser response-header policy; configure CORS middleware separately when browser JavaScript must read cross-origin responses.
|
||||
|
||||
When you deploy behind a public hostname, add the hostname clients use to reach your MCP endpoint. If a browser-based MCP client runs on a separate origin, add that origin as well:
|
||||
Enable strict validation with `host_origin_protection=True`. When you deploy behind a public hostname, add the hostname clients use to reach your MCP endpoint. If a browser-based MCP client runs on a separate origin, add that origin as well:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
|
@ -115,6 +115,7 @@ from fastmcp import FastMCP
|
|||
mcp = FastMCP("My Server")
|
||||
|
||||
app = mcp.http_app(
|
||||
host_origin_protection=True,
|
||||
allowed_hosts=["mcp.example.com"],
|
||||
allowed_origins=["https://app.example.com"],
|
||||
)
|
||||
|
|
@ -132,6 +133,7 @@ if __name__ == "__main__":
|
|||
transport="http",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
host_origin_protection=True,
|
||||
allowed_hosts=["mcp.example.com"],
|
||||
allowed_origins=["https://app.example.com"],
|
||||
)
|
||||
|
|
@ -140,11 +142,12 @@ if __name__ == "__main__":
|
|||
You can also configure these values with environment variables:
|
||||
|
||||
```bash
|
||||
export FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=true
|
||||
export FASTMCP_HTTP_ALLOWED_HOSTS='["mcp.example.com"]'
|
||||
export FASTMCP_HTTP_ALLOWED_ORIGINS='["https://app.example.com"]'
|
||||
```
|
||||
|
||||
Use `host_origin_protection=False` only for trusted internal deployments that provide equivalent validation at another layer, such as an ingress proxy.
|
||||
Use `host_origin_protection="auto"` to protect localhost-bound direct servers while allowing ASGI, serverless, and reverse-proxy deployments to keep their existing Host handling unless they configure explicit trust rules. Use `host_origin_protection=False` to keep the request guard disabled.
|
||||
|
||||
### Health Checks
|
||||
|
||||
|
|
@ -201,7 +204,7 @@ Most MCP clients, including those that you access through a browser like ChatGPT
|
|||
|
||||
CORS (Cross-Origin Resource Sharing) is needed when JavaScript running in a web browser connects directly to your MCP server. This is different from using an LLM through a browser—in that case, the browser connects to the LLM service, and the LLM service connects to your MCP server (no CORS needed).
|
||||
|
||||
Host and Origin protection runs before CORS. Add browser client origins to `allowed_origins` so trusted browser requests reach the CORS middleware, then configure CORS to let browser JavaScript read the MCP response headers it needs. Setting `allowed_origins` trusts the request; it does not emit `Access-Control-Allow-Origin` or other CORS response headers.
|
||||
Host and Origin protection runs before CORS when it is active for a request. Add browser client origins to `allowed_origins` so trusted browser requests reach the CORS middleware, then configure CORS to let browser JavaScript read the MCP response headers it needs. Setting `allowed_origins` trusts the request; it does not emit `Access-Control-Allow-Origin` or other CORS response headers.
|
||||
|
||||
Browser-based MCP clients that need CORS include:
|
||||
|
||||
|
|
@ -342,7 +345,7 @@ def analyze(data: str) -> dict:
|
|||
return {"result": f"Analyzed: {data}"}
|
||||
|
||||
# Create the ASGI app
|
||||
mcp_app = mcp.http_app(path='/mcp')
|
||||
mcp_app = mcp.http_app(path="/mcp")
|
||||
|
||||
# Create a Starlette app and mount the MCP server
|
||||
app = Starlette(
|
||||
|
|
@ -354,7 +357,7 @@ app = Starlette(
|
|||
)
|
||||
```
|
||||
|
||||
The MCP endpoint will be available at `/mcp-server/mcp/` of the resulting Starlette app.
|
||||
The MCP endpoint will be available at `/mcp-server/mcp` of the resulting Starlette app.
|
||||
|
||||
<Warning>
|
||||
For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the resulting Starlette app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized.
|
||||
|
|
@ -373,7 +376,7 @@ from starlette.routing import Mount
|
|||
mcp = FastMCP("MyServer")
|
||||
|
||||
# Create the ASGI app
|
||||
mcp_app = mcp.http_app(path='/mcp')
|
||||
mcp_app = mcp.http_app(path="/mcp")
|
||||
|
||||
# Create nested application structure
|
||||
inner_app = Starlette(routes=[Mount("/inner", app=mcp_app)])
|
||||
|
|
@ -383,7 +386,7 @@ app = Starlette(
|
|||
)
|
||||
```
|
||||
|
||||
In this setup, the MCP server is accessible at the `/outer/inner/mcp/` path.
|
||||
In this setup, the MCP server is accessible at the `/outer/inner/mcp` path.
|
||||
|
||||
### FastAPI Integration
|
||||
|
||||
|
|
@ -738,9 +741,7 @@ If you're using the [OAuth Proxy](/servers/auth/oauth-proxy), FastMCP issues its
|
|||
|
||||
**Default Behavior (Development Only):**
|
||||
|
||||
By default, FastMCP automatically manages cryptographic keys:
|
||||
- **Mac/Windows**: Keys are generated and stored in your system keyring, surviving server restarts. Suitable **only** for development and local testing.
|
||||
- **Linux**: Keys are ephemeral (random salt at startup), so tokens are invalidated on restart.
|
||||
By default, FastMCP automatically manages cryptographic keys the same way on every platform: the signing key is deterministically derived from your OAuth client secret, so it survives server restarts as long as the secret doesn't change. Suitable **only** for development and local testing.
|
||||
|
||||
This automatic approach is convenient for development but not suitable for production deployments.
|
||||
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ if __name__ == "__main__":
|
|||
mcp.run(transport="http") # Health check at http://localhost:8000/health
|
||||
```
|
||||
|
||||
Custom routes are served by the same web server as your MCP endpoint. They're available at the root of your domain while the MCP endpoint is at `/mcp/`. For more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/deployment/http#integration-with-web-frameworks).
|
||||
Custom routes are served by the same web server as your MCP endpoint. They're available at the root of your domain while the MCP endpoint is at `/mcp`. For more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/deployment/http#integration-with-web-frameworks).
|
||||
|
||||
## Alternative Initialization Patterns
|
||||
|
||||
|
|
|
|||
|
|
@ -229,9 +229,10 @@ Environment variables are included in this section because they're runtime confi
|
|||
|
||||
<Expandable title="Deployment Fields">
|
||||
<ParamField body="transport" type="string" default="stdio">
|
||||
Protocol for client communication:
|
||||
Protocol for client communication. `"http"` and `"streamable-http"` both select FastMCP's Streamable HTTP transport:
|
||||
- `"stdio"`: Standard input/output for desktop clients
|
||||
- `"http"`: Network-accessible HTTP server
|
||||
- `"http"`: Network-accessible Streamable HTTP server
|
||||
- `"streamable-http"`: Explicit alias for Streamable HTTP
|
||||
- `"sse"`: Server-sent events
|
||||
</ParamField>
|
||||
|
||||
|
|
@ -241,12 +242,12 @@ Environment variables are included in this section because they're runtime confi
|
|||
- `"0.0.0.0"`: All network interfaces
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="port" type="integer" default="3000">
|
||||
Port number for HTTP transport.
|
||||
<ParamField body="port" type="integer" default="8000">
|
||||
Port number for HTTP transport. If omitted, FastMCP uses the server runtime default.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="path" type="string" default="/mcp/">
|
||||
URL path for the MCP endpoint when using HTTP transport.
|
||||
<ParamField body="path" type="string" default="/mcp">
|
||||
URL path for the MCP endpoint when using HTTP transport. The default is `/mcp` for Streamable HTTP and `/sse` for SSE.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="log_level" type="string" default="INFO">
|
||||
|
|
@ -396,20 +397,20 @@ This flag tells FastMCP: "I already have the source code, skip any download/clon
|
|||
|
||||
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:
|
||||
The configuration file works with server-loading commands that explicitly accept FastMCP config files:
|
||||
- **`run`** - Start the server in production mode
|
||||
- **`dev`** - Launch with the Inspector UI for development
|
||||
- **`dev inspector`** - Launch with the Inspector UI for development
|
||||
- **`inspect`** - View server capabilities and configuration
|
||||
- **`install`** - Install to Claude Desktop, Cursor, or other MCP clients
|
||||
- **`install`** - Install to Claude Desktop, Cursor, or another MCP client
|
||||
|
||||
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.
|
||||
`run`, `dev inspector`, and `inspect` search the current directory for a file named exactly `fastmcp.json` when you don't pass a file argument, so you can navigate to your project directory and run `fastmcp run` to start your server with all its configured settings. `install` requires an explicit path to the config file — it never searches.
|
||||
|
||||
### 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
|
||||
# Config specifies port 8000, CLI overrides to 8080
|
||||
fastmcp run fastmcp.json --port 8080
|
||||
|
||||
# Config specifies stdio, CLI overrides to HTTP
|
||||
|
|
@ -434,7 +435,7 @@ You can use different configuration files for different environments:
|
|||
- `prod.fastmcp.json` - Production settings
|
||||
- `test_fastmcp.json` - Test configuration
|
||||
|
||||
Any file with "fastmcp.json" in the name is recognized as a configuration file.
|
||||
Only a file named exactly `fastmcp.json` is auto-detected when you omit the path. Other FastMCP configuration files can use any `.json` name, but you must pass them explicitly.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
@ -471,7 +472,7 @@ A configuration optimized for local development:
|
|||
"type": "uv",
|
||||
"python": "3.12",
|
||||
"dependencies": ["fastmcp[dev]"],
|
||||
"editable": "."
|
||||
"editable": ["."]
|
||||
},
|
||||
// HOW should it run?
|
||||
"deployment": {
|
||||
|
|
@ -510,7 +511,7 @@ A production-ready configuration with full dependency management:
|
|||
"transport": "http",
|
||||
"host": "0.0.0.0",
|
||||
"port": 3000,
|
||||
"path": "/api/mcp/",
|
||||
"path": "/api/mcp",
|
||||
"log_level": "INFO",
|
||||
"env": {
|
||||
"ENV": "production",
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ Tests are documentation that shows how features work. Good tests give reviewers
|
|||
uv run pytest tests/server/ -v
|
||||
|
||||
# Run all tests before submitting PR
|
||||
uv run pytest
|
||||
uv run pytest -n auto
|
||||
```
|
||||
|
||||
Every new feature needs tests. See the [Testing Guide](/development/tests) for patterns and requirements.
|
||||
|
|
@ -166,7 +166,7 @@ just api-ref-all
|
|||
|
||||
#### Before Submitting
|
||||
|
||||
1. **Run all checks**: `uv run prek run --all-files && uv run pytest`
|
||||
1. **Run all checks**: `uv run prek run --all-files && uv run pytest -n auto`
|
||||
2. **Keep scope small**: One feature or fix per PR
|
||||
3. **Write clear description**: Your PR description becomes permanent documentation
|
||||
4. **Update docs**: Include documentation for API changes
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ Major versions represent fundamental shifts. FastMCP 2.x is entirely different f
|
|||
Unlike traditional semantic versioning, minor versions **may** include [breaking changes](#breaking-changes) when necessary for the ecosystem's evolution. This flexibility is essential in a young ecosystem where perfect backwards compatibility would prevent important improvements.
|
||||
</Warning>
|
||||
|
||||
FastMCP always targets the most current MCP Protocol version. Breaking changes in the MCP spec or MCP SDK automatically flow through to FastMCP - we prioritize staying current with the latest features and conventions over maintaining compatibility with older protocol versions.
|
||||
FastMCP tracks the current MCP Protocol version while serving earlier handshake versions alongside it. Building on MCP SDK v2, a FastMCP server negotiates the protocol era each client speaks — the sessionless `2026-07-28` era and earlier session-based eras are both handled by the same server. New features and conventions from the spec flow through to FastMCP as they land; for the details of which capabilities are available on each era, see [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3#protocol-version-support).
|
||||
|
||||
**Patch (2.0.x)**: Bug fixes and refinements
|
||||
|
||||
|
|
@ -65,6 +65,8 @@ Our release process is intentionally simple:
|
|||
2. Generate release notes automatically, and curate or add additional editorial information as needed
|
||||
3. GitHub releases automatically trigger PyPI deployments
|
||||
|
||||
Current-major releases target `main`. Maintenance releases target their release branch, such as `release/3.x` for 3.x patches and `release/2.x` for 2.x patches. Stable releases from `main` update the `published-docs` branch after PyPI publishing succeeds; maintenance releases publish packages and GitHub release notes without repointing the live docs branch.
|
||||
|
||||
This automation lets maintainers focus on code quality rather than release mechanics.
|
||||
|
||||
### Release Cadence
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ Good tests are the foundation of reliable software. In FastMCP, we treat tests a
|
|||
|
||||
```bash
|
||||
# Run all tests
|
||||
uv run pytest
|
||||
uv run pytest -n auto
|
||||
|
||||
# Run specific test file
|
||||
uv run pytest tests/server/test_auth.py
|
||||
|
|
@ -26,7 +26,7 @@ uv run pytest --cov=fastmcp
|
|||
uv run pytest -m "not integration"
|
||||
|
||||
# Skip tests that spawn processes
|
||||
uv run pytest -m "not integration and not client_process"
|
||||
uv run pytest -m "not integration and not client_process and not subprocess_heavy"
|
||||
```
|
||||
|
||||
Tests should complete in under 1 second unless marked as integration tests. This speed encourages running them frequently, catching issues early.
|
||||
|
|
@ -61,6 +61,40 @@ async def test_stdio_transport():
|
|||
assert result.content[0].text == "test"
|
||||
```
|
||||
|
||||
A third marker, `subprocess_heavy`, exists specifically for Windows CI stability. See [Windows CI and Test Parallelism](#windows-ci-and-test-parallelism) below for when to use it and why it exists.
|
||||
|
||||
### Windows CI and Test Parallelism
|
||||
|
||||
Windows CI ran the unit suite serially for months. [#2715](https://github.com/PrefectHQ/fastmcp/pull/2715) tried enabling `pytest-xdist` parallelism there in December 2025; [#2726](https://github.com/PrefectHQ/fastmcp/pull/2726) reverted it the next day because "Windows tests continue to fail with intermittent worker crashes." [#4554](https://github.com/PrefectHQ/fastmcp/pull/4554) re-enabled it after removing most of the subprocess pressure that caused those crashes, taking the Windows unit step from roughly 460s to 175s.
|
||||
|
||||
That pressure came from three sources, all addressed by #4554: most HTTP tests moved in-process via `asgi_client` instead of binding real sockets, stdio lifecycle tests spawn a minimal stdlib responder (`tests/client/minimal_stdio_server.py`, ~0.03s to start) instead of a subprocess that runs `import fastmcp` (~0.7s), and roughly 80 real `sleep()` calls became deterministic waits on the condition each test actually cared about. Fewer, cheaper subprocesses competing under parallel workers left fewer chances for a worker to die.
|
||||
|
||||
#### The `subprocess_heavy` marker
|
||||
|
||||
One class of test still spawns a full Python interpreter that imports FastMCP — checking that a bare install doesn't need optional dependencies, or that a decorator works from a fresh process. Each spawn pays a full interpreter's startup and memory footprint, and a 2-core Windows runner already running 2 xdist workers has little headroom left to absorb that. These tests carry `@pytest.mark.subprocess_heavy` and run in the existing serial `client_process` CI step instead of alongside the parallel workers — `.github/actions/run-pytest/action.yml` routes `client_process or subprocess_heavy` to that step (`MAX_PROCS=0`) and excludes both markers from the parallel unit step.
|
||||
|
||||
If a test runs `subprocess.run([sys.executable, "-c", ...])`, or otherwise starts a fresh interpreter that imports `fastmcp`, mark it `subprocess_heavy`. A subprocess that runs a minimal stdlib script with no FastMCP import doesn't need the marker — it's the interpreter startup and import that's expensive, not the subprocess itself.
|
||||
|
||||
#### This is a mitigation, not a proof
|
||||
|
||||
There is no root-cause diagnosis behind this fix, only a plausible one. During validation, one Windows run genuinely crashed a worker on `test_fastmcp_imports_without_legacy_httpx` — a fresh-interpreter test — with pytest-xdist reporting `worker 'gw1' crashed while running '...'` after execnet's channel saw `ConnectionResetError: [WinError 10054]`. Nothing in that log says *why* the worker died: memory exhaustion, handle exhaustion, and some Windows-specific `subprocess`/`execnet` interaction are all still consistent with what was observed. Marking the fresh-interpreter tests `subprocess_heavy` made the crash stop recurring, but "it stopped" is not the same as "we know why."
|
||||
|
||||
Treat the next Windows worker crash as a test of this diagnosis. **If it lands on a test that is not a fresh-interpreter spawner, the `subprocess_heavy` theory was wrong** — the real problem is subprocess-under-xdist on Windows more generally, and isolating one marker's worth of tests was never going to fix that. The fallback is one conditional back in `run-pytest/action.yml`, restoring the pre-#4554 behavior:
|
||||
|
||||
```bash
|
||||
PARALLEL_FLAGS=""
|
||||
if [ "$MAX_PROCS" != "0" ] && [ "${{ runner.os }}" != "Windows" ]; then
|
||||
PARALLEL_FLAGS="--numprocesses auto --maxprocesses $MAX_PROCS --dist worksteal"
|
||||
fi
|
||||
```
|
||||
|
||||
#### Two traps that aren't Windows-specific
|
||||
|
||||
Two test-authoring bugs surfaced while validating this change. Neither is about Windows or parallelism, but both are worth watching for anywhere a real `sleep()` gets replaced with a wait:
|
||||
|
||||
- **Match the wait condition to the assertion.** A test waited for "any callback fired," then asserted that a `completed` callback existed. That races, because an earlier `working` notification satisfies the wait before the `completed` one arrives. A deterministic wait is only as good as the condition it waits on — wait for the thing you actually assert.
|
||||
- **Don't assert on incidental timing.** A crash-recovery test asserted "at least one concurrent request fails" while a subprocess restarts, which quietly depended on the restart being slow. Once restart got faster, recovery could beat every in-flight request and the test started failing because the behavior *improved*. Assert the invariant instead: no hang, and no result served by the dead process.
|
||||
|
||||
## Writing Tests
|
||||
|
||||
|
||||
|
|
@ -228,7 +262,7 @@ async def test_tool_schema_generation():
|
|||
return {"amount": amount, "tax": amount * rate, "total": amount * (1 + rate)}
|
||||
|
||||
tools = mcp.list_tools()
|
||||
schema = tools[0].inputSchema
|
||||
schema = tools[0].input_schema
|
||||
|
||||
# First run: snapshot() is empty, gets auto-populated
|
||||
# Subsequent runs: compares against stored snapshot
|
||||
|
|
@ -299,22 +333,19 @@ async def test_database_tool():
|
|||
|
||||
### Testing Network Transports
|
||||
|
||||
While in-memory testing covers most unit testing needs, you'll occasionally need to test actual network transports like HTTP or SSE. FastMCP provides two approaches: in-process async servers (preferred), and separate subprocess servers (for special cases).
|
||||
In-memory testing covers most unit testing needs, but some behavior only exists over HTTP: middleware, authentication, session management, header handling, and SSE streaming. To test those, serve your server over HTTP with `asgi_client`.
|
||||
|
||||
#### In-Process Network Testing (Preferred)
|
||||
#### Testing Over HTTP
|
||||
|
||||
<VersionBadge version="2.13.0" />
|
||||
<VersionBadge version="3.5.0" />
|
||||
|
||||
For most network transport tests, use `run_server_async` as an async context manager. This runs the server as a task in the same process, providing fast, deterministic tests with full debugger support:
|
||||
`asgi_client` builds your server's real Starlette app, starts its lifespan, and hands you a connected `Client` that talks to it over the full HTTP stack. The one thing it skips is the socket: requests are dispatched straight into the ASGI application on the current event loop, so there is no port to bind, no uvicorn to start, and no connection to negotiate. Everything else — middleware, authentication, session management, SSE framing — runs exactly as it does in production.
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from fastmcp import FastMCP, Client
|
||||
from fastmcp.client.transports import StreamableHttpTransport
|
||||
from fastmcp.utilities.tests import run_server_async
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.utilities.tests import asgi_client
|
||||
|
||||
def create_test_server() -> FastMCP:
|
||||
"""Create a test server instance."""
|
||||
server = FastMCP("TestServer")
|
||||
|
||||
@server.tool
|
||||
|
|
@ -323,26 +354,89 @@ def create_test_server() -> FastMCP:
|
|||
|
||||
return server
|
||||
|
||||
@pytest.fixture
|
||||
async def http_server() -> str:
|
||||
"""Start server in-process for testing."""
|
||||
server = create_test_server()
|
||||
async with run_server_async(server) as url:
|
||||
yield url
|
||||
|
||||
async def test_http_transport(http_server: str):
|
||||
"""Test actual HTTP transport behavior."""
|
||||
async with Client(
|
||||
transport=StreamableHttpTransport(http_server)
|
||||
) as client:
|
||||
result = await client.ping()
|
||||
assert result is True
|
||||
|
||||
async def test_greet_over_http():
|
||||
async with asgi_client(create_test_server()) as client:
|
||||
greeting = await client.call_tool("greet", {"name": "World"})
|
||||
assert greeting.data == "Hello, World!"
|
||||
```
|
||||
|
||||
The `run_server_async` context manager automatically handles server lifecycle and cleanup. This approach is faster than subprocess-based testing and provides better error messages.
|
||||
Pass `transport="sse"` to exercise the SSE app instead of streamable HTTP, `path=` to serve on a custom path, `headers=` and `auth=` to configure the client's requests, and any other keyword argument to configure the `Client` itself.
|
||||
|
||||
```python
|
||||
async def test_tenant_header_is_visible_to_tools():
|
||||
async with asgi_client(
|
||||
create_test_server(),
|
||||
headers={"X-Tenant-ID": "acme"},
|
||||
timeout=5,
|
||||
) as client:
|
||||
await client.list_tools()
|
||||
```
|
||||
|
||||
#### Sharing One Server Across Tests
|
||||
|
||||
When several tests share a server but each needs its own client, use `asgi_server` in a fixture. It yields an `ASGIServer`, whose `client()` method produces a fresh client — with its own session — on demand.
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.utilities.tests import ASGIServer, asgi_server
|
||||
|
||||
@pytest.fixture
|
||||
async def http_server():
|
||||
server = FastMCP("TestServer")
|
||||
|
||||
@server.tool
|
||||
def greet(name: str) -> str:
|
||||
return f"Hello, {name}!"
|
||||
|
||||
async with asgi_server(server) as running_server:
|
||||
yield running_server
|
||||
|
||||
async def test_greet(http_server: ASGIServer):
|
||||
async with http_server.client() as client:
|
||||
greeting = await client.call_tool("greet", {"name": "World"})
|
||||
assert greeting.data == "Hello, World!"
|
||||
|
||||
async def test_sessions_are_isolated(http_server: ASGIServer):
|
||||
async with (
|
||||
http_server.client(mode="legacy") as first,
|
||||
http_server.client(mode="legacy") as second,
|
||||
):
|
||||
assert await first.ping() is True
|
||||
assert await second.ping() is True
|
||||
```
|
||||
|
||||
Sessions belong to the handshake era of the MCP protocol, and so does `ping`, so a test that is about session behavior pins `mode="legacy"`. Every keyword argument `client()` doesn't consume itself is passed straight to `Client`. See [protocol negotiation](/clients/client#protocol-negotiation).
|
||||
|
||||
For assertions about raw HTTP — status codes, response headers, metadata endpoints — `http_client()` returns an `httpx.AsyncClient` bound to the same app. Because nothing is listening on the network, this is the only way to make raw requests; a plain `httpx.AsyncClient()` cannot reach the server.
|
||||
|
||||
```python
|
||||
async def test_unauthenticated_request_is_rejected(http_server: ASGIServer):
|
||||
async with http_server.http_client() as http:
|
||||
response = await http.post(http_server.url, json={"jsonrpc": "2.0", "id": 1})
|
||||
assert response.status_code in (400, 401)
|
||||
```
|
||||
|
||||
If you need to build the client transport yourself, `transport()` returns a `StreamableHttpTransport` or `SSETransport` already wired to the in-process app.
|
||||
|
||||
#### Testing on a Real Port
|
||||
|
||||
<VersionBadge version="2.13.0" />
|
||||
|
||||
`run_server_async` starts a real uvicorn server on a real TCP port as a task in the current process and yields its URL. Reach for it only when the subject of the test is the network itself — real sockets, TLS, or a server that must be reachable by something other than an in-process client.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP, Client
|
||||
from fastmcp.utilities.tests import run_server_async
|
||||
|
||||
async def test_server_binds_a_real_port():
|
||||
server = FastMCP("TestServer")
|
||||
|
||||
async with run_server_async(server) as url:
|
||||
assert url.startswith("http://127.0.0.1:")
|
||||
async with Client(url) as client:
|
||||
assert await client.list_tools() == []
|
||||
```
|
||||
|
||||
#### Subprocess Testing (Special Cases)
|
||||
|
||||
|
|
@ -375,8 +469,8 @@ async def test_http_transport(http_server: str):
|
|||
async with Client(
|
||||
transport=StreamableHttpTransport(http_server)
|
||||
) as client:
|
||||
result = await client.ping()
|
||||
assert result is True
|
||||
tools = await client.list_tools()
|
||||
assert "greet" in [tool.name for tool in tools]
|
||||
```
|
||||
|
||||
The `run_server_in_process` utility handles server lifecycle, port allocation, and cleanup automatically. Use this only when subprocess isolation is truly necessary, as it's slower and harder to debug than in-process testing. FastMCP uses the `client_process` marker to isolate these tests in CI.
|
||||
|
|
|
|||
|
|
@ -444,7 +444,7 @@ async def dashboard(ctx: Context) -> dict:
|
|||
|
||||
**Future phases** will add a component DSL for building UIs declaratively, an in-repo renderer, and a `FastMCPApp` class.
|
||||
|
||||
Implementation: `fastmcp_slim/fastmcp/server/apps.py` (models and constants), with integration points in `server.py` (decorator parameters), `low_level.py` (extension advertisement), and `context.py` (`client_supports_extension` method).
|
||||
Implementation: `fastmcp_slim/fastmcp/apps/config.py` (models and constants), with integration points in `server.py` (decorator parameters), `low_level.py` (extension advertisement), and `context.py` (`client_supports_extension` method).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -939,7 +939,7 @@ v3.0 implements MCP SEP-1686 for background task execution via Docket integratio
|
|||
**Configuration** (`fastmcp_slim/fastmcp/server/tasks/config.py`):
|
||||
|
||||
```python
|
||||
from fastmcp.server.tasks import TaskConfig
|
||||
from fastmcp.utilities.tasks import TaskConfig
|
||||
|
||||
@mcp.tool(task=TaskConfig(mode="required"))
|
||||
async def long_running_task():
|
||||
|
|
@ -1205,8 +1205,8 @@ When `list_page_size` is set, `tools/list`, `resources/list`, `resources/templat
|
|||
```python
|
||||
async with Client(server) as client:
|
||||
result = await client.list_tools_mcp()
|
||||
while result.nextCursor:
|
||||
result = await client.list_tools_mcp(cursor=result.nextCursor)
|
||||
while result.next_cursor:
|
||||
result = await client.list_tools_mcp(cursor=result.next_cursor)
|
||||
```
|
||||
|
||||
Documentation: [Pagination](/servers/pagination)
|
||||
|
|
|
|||
153
docs/development/v4-notes/background-tasks.mdx
Normal file
153
docs/development/v4-notes/background-tasks.mdx
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
---
|
||||
title: Background Tasks (SEP-2663)
|
||||
---
|
||||
|
||||
**Status: Shipped (#4602, #4603).** This page is the approved design for rebuilding FastMCP's background-task support on the `io.modelcontextprotocol/tasks` extension. It supersedes the earlier "delete the task machinery" direction recorded during the SDK v2 migration. The [Feature Program](/development/v4-notes/feature-program#background-tasks-sep-2663) carries the one-line status; user-facing usage is documented at [Background Tasks](/servers/tasks) and [Background Tasks (client)](/clients/tasks).
|
||||
|
||||
## TL;DR
|
||||
|
||||
Background tasks live on. The MCP spec moved them out of core and into a **Final, merged** extension — `io.modelcontextprotocol/tasks` (SEP-2663) — that keeps the polling model FastMCP already implements. **No SDK, in any language, ships a runtime for it yet.** FastMCP owns the only production-shaped execution engine (Docket/Redis) built for a near-identical protocol.
|
||||
|
||||
The plan: **rebuild task support on SEP-2663 as `fastmcp-tasks`, an in-repo optional package**, gated by `task=True` exactly as MCP Apps is gated by `app=True`. Remove the SEP-1686 *wire layer*; keep and re-home the *execution engine*. Along the way, introduce a **FastMCP-native server extension API** so tasks (and later Apps) plug in through one documented mechanism instead of bespoke surgery on core.
|
||||
|
||||
Net effect: a server that already uses `@mcp.tool(task=True)` needs **no code change**, and FastMCP plausibly becomes the first runtime implementation of the tasks extension anywhere.
|
||||
|
||||
## Background: where tasks stand today
|
||||
|
||||
FastMCP 3 shipped background tasks against **SEP-1686**, the task protocol that briefly lived in the core MCP spec. The implementation is ~4,000 lines across server, client, CLI, and an SDK shim, split into two very different halves:
|
||||
|
||||
- **A wire layer** — capability advertisement, the `tasks/get|result|list|cancel` handlers, a `CreateTaskResult` on augmented `tools/call`, and a Redis-backed *push* relay that lets a worker reach a client to deliver notifications and elicitation requests.
|
||||
- **An execution engine** — [Docket](https://github.com/chrisguidry/docket) (queue, worker, result store, TTL, `memory://` or `redis://` backends) plus FastMCP-built durability: auth-scoped compound keys that isolate task access by caller, request-context snapshot/restore across worker processes, argument-coercion parity with the sync path, and the `fastmcp tasks worker` CLI.
|
||||
|
||||
The SDK v2 migration removed SEP-1686 from the core spec. The v4 design notes, until now, recorded the consequence as "delete the task machinery; users who need tasks stay on FastMCP 3." That was the right call **given the information at the time** — the assumption was that the successor protocol either didn't exist or wasn't implementable. Both halves of that assumption turned out to be wrong.
|
||||
|
||||
## What changed upstream: SEP-2663
|
||||
|
||||
Tasks were reworked, not removed. **SEP-2663 ("Tasks Extension") is Final and was merged upstream on 2026-05-15**, superseding SEP-1686. It defines the `io.modelcontextprotocol/tasks` extension, a capability-negotiated feature layered on the SEP-2133 extensions mechanism. It keeps SEP-1686's polling core and tightens it.
|
||||
|
||||
**The wire shape:**
|
||||
|
||||
1. Client advertises the tasks capability (per-request, in `_meta`). This is *consent* — "I can handle a task result" — not a request to run one.
|
||||
2. Client issues a normal `tools/call`. **The server decides** whether to run it as a task.
|
||||
3. If tasked, the server returns a `CreateTaskResult` (a claimed result shape carrying `resultType: "task"`) with a **server-generated** `taskId`.
|
||||
4. Client polls `tasks/get` until the status is terminal; the result is **inlined** into that response.
|
||||
5. In-task input (elicit/sample/roots requested *during* execution) is **poll-based**: status flips to `input_required`, outstanding requests appear in an `inputRequests` map, and the client answers via `tasks/update`.
|
||||
6. `tasks/cancel` is cooperative. Optional push exists (`notifications/tasks` over `subscriptions/listen`) but servers need not send it.
|
||||
|
||||
**Delta from SEP-1686** — and the striking thing is that most of it is *deletion*, because the spec moved toward what FastMCP already built:
|
||||
|
||||
| Dimension | SEP-1686 (old) | SEP-2663 (new) | FastMCP today |
|
||||
| --- | --- | --- | --- |
|
||||
| Task-id generation | Client-generated | **Server**-generated | Already server-generated |
|
||||
| `tasks/list` | Present | **Removed** (enumeration risk) | Already a stub returning `[]` |
|
||||
| Result retrieval | Separate `tasks/result` | **Inlined** into `tasks/get` | Merge two handlers into one |
|
||||
| `tasks/delete` | Present | **Removed** (rely on TTL) | TTL is Docket-native |
|
||||
| Creation race | `notifications/tasks/created` | **Durable-creation MUST** | One read-your-writes check away |
|
||||
| In-task input | Push relay + `_meta` tagging | **Poll**: `input_required` + `tasks/update` | Replaces the hairiest module |
|
||||
| Statuses | 7 (incl. `submitted`, `unknown`) | 5 | Shrinks a mapping table |
|
||||
| Augmentable requests | Any | **`tools/call` only** | Tools-only surface (see scope) |
|
||||
| LB routing | Unspecified | `Mcp-Name: <taskId>` header | Moot with shared Redis |
|
||||
|
||||
**Critically: no runtime exists.** The `ext-tasks` repo is schema + prose only. The TypeScript and Python SDKs carry the wire types and conformance fixtures — no client/server implementation. The field is open.
|
||||
|
||||
## The decision
|
||||
|
||||
**Build it.** Two facts flip the earlier "delete and wait" call:
|
||||
|
||||
1. **The spec is what FastMCP already implements**, minus a push relay it can now shed. The rebuild is dominated by deletion and a thin new wire adapter, not a from-scratch effort.
|
||||
2. **FastMCP is uniquely positioned.** SEP-2663 *assumes* a durable server-side store, server-minted high-entropy ids, eventual-consistency-aware creation, and multi-node routing — precisely what Docket/Redis provides. No other framework has this built.
|
||||
|
||||
Maintaining the SEP-1686 machinery through the migration is dead weight (it's the sole reason for the `_sdk_patches.py` shim, the `TaskNotificationHandler`, and a cluster of protocol-era xfails). Rebuilding on SEP-2663 clears that debt *and* produces a flagship v4 capability with a zero-code-change migration story.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Engine and wire split
|
||||
|
||||
The existing code already separates cleanly along this line; the rebuild makes the boundary a package boundary.
|
||||
|
||||
- **Removed:** the SEP-1686 wire layer — capability advertisement, the four CRUD handlers, and (the big win) the entire Redis push relay (`server/tasks/elicitation.py`, `notifications.py`), which existed only because SEP-1686 had no poll-based in-task input channel. SEP-2663's `input_required`/`tasks/update` replaces it; the request/response store survives, the push envelope does not.
|
||||
- **Kept and re-homed:** the Docket execution engine, the auth-scoped key encoding (this is our *authorization* layer for `tasks/get`/`update`/`cancel` — stronger than the spec's "taskIds may be bearer tokens"), context snapshot/restore, argument coercion, and the worker CLI. All of it is wire-agnostic.
|
||||
- **New:** a thin SEP-2663 wire adapter — capability, the `tasks/get`/`update`/`cancel` methods, and a `tools/call` interceptor that decides-and-tasks.
|
||||
|
||||
### Packaging
|
||||
|
||||
`fastmcp-tasks` becomes an in-repo `uv` workspace member on the `fastmcp_remote` template (own `pyproject.toml`, lockstep-versioned, re-exported through the `fastmcp` metapackage). The DX parallel with MCP Apps is exact:
|
||||
|
||||
| Concern | MCP Apps | Background tasks |
|
||||
| --- | --- | --- |
|
||||
| Authoring flag (core) | `@mcp.tool(app=True)` | `@mcp.tool(task=True)` |
|
||||
| Optional package | `prefab-ui` | `fastmcp-tasks` |
|
||||
| Extra | `fastmcp[apps]` | `fastmcp[tasks]` |
|
||||
| Missing-package behavior | Loud install hint | Loud install hint at server build |
|
||||
|
||||
**Core keeps only the declaration:** `task=True` / `TaskConfig` is metadata on a component, with no engine import. Everything else — engine and wire adapter — lives in the `fastmcp-tasks` package. The existing `[tasks]` extra re-points from the SEP-1686 machinery to `fastmcp-tasks`, so `pip install fastmcp[tasks]` and `task=True` keep working with modern wire underneath.
|
||||
|
||||
Activation stays **implicit-but-loud** (the existing `require_docket()` pattern, not silent degradation): `task=True` anywhere triggers a lazy import of `fastmcp-tasks` at build time; a missing install raises immediately. A tool the author marked as a task silently running inline would be a correctness bug, not a graceful fallback.
|
||||
|
||||
### The extension API
|
||||
|
||||
MCP extensions (SEP-2133) are a **genuinely new abstraction in SDK v2** — they did not exist in v1. So MCP Apps hand-rolling its integration wasn't a wrong choice; it predates the tool. Today FastMCP's **server** bypasses the SDK's `Extension` class entirely (it hand-splices the `ui` capability onto the low-level server and walks tool metadata directly), while the **client** forwards `ClientExtension` natively. Every new protocol extension currently means bespoke core surgery.
|
||||
|
||||
Tasks is the forcing function to fix that. The design adds a single registration point:
|
||||
|
||||
```python test="skip"
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp_tasks import TasksExtension
|
||||
|
||||
mcp = FastMCP("Server")
|
||||
mcp.add_extension(TasksExtension(url="redis://...")) # required to enable tasks
|
||||
|
||||
|
||||
@mcp.tool(task=True) # intent: this tool CAN run as a task
|
||||
async def crunch(dataset: str) -> str:
|
||||
...
|
||||
```
|
||||
|
||||
`add_extension` is **required** for `task=True` to work — it is not autodetected from the presence of `task=True` flags. This is deliberate. The extension needs configuration that has to live somewhere (backend URL, worker concurrency, TTL defaults), and `add_extension(TasksExtension(...))` is its natural home; autodetection would only scatter that config into settings/env and hide the moment of enablement. Requiring it also keeps capability advertisement honest — the server advertises the `tasks` capability iff the extension is registered — and removes the worst footgun, a tool silently running on an in-memory backend in production because nobody configured Redis. The two concerns stay cleanly separated: `task=True` is per-component intent ("this tool *can* be a task"); `add_extension` is server-wide enablement and config ("this server *runs* tasks, here's how"). Using `task=True` with no extension registered is a loud build-time error.
|
||||
|
||||
The extension API contributes a negotiated capability, additive request methods, and a `tools/call` interceptor — with access to FastMCP-level constructs the SDK's `Extension` withholds (the component registry, `Context`, auth scope). It is **designed against tasks** because tasks exercises the full surface (capability + methods + interception + client claims + notifications), where Apps exercises only a subset. Apps migrates onto the extension API as a fast-follow, deleting the hand-rolled splices and confirming the design generalizes.
|
||||
|
||||
**Extension vs. middleware** — the discriminator, so we do not over-apply this: an extension is a *negotiated contract change the client must understand*; middleware is *unilateral server behavior the client never sees*. PII detection, auth, rate limiting → [middleware](/servers/middleware). Tasks, Apps → extensions. Litmus test: delete the capability advertisement — if nothing about the client's behavior changes, it was middleware.
|
||||
|
||||
### Client experience
|
||||
|
||||
SEP-2663 removed the client-side "make this a task" flag — the server decides. That maps onto FastMCP's existing two-tier client surface, the **friendly** `call_tool` vs the **low-level** `call_tool_mcp`, so there is almost no new API:
|
||||
|
||||
- **`call_tool(name, args)` (friendly)** — advertises the capability and, if the server tasks the call, **transparently drives the poll loop** and returns the finished result. Whether the server tasked it is invisible. The machinery already exists: the migration wired claim-resolution through `call_tool_mcp`'s `allow_claimed` path, so a returned `CreateTaskResult` is finished into an ordinary `CallToolResult`. In-task `input_required` routes through the client's **existing elicitation handler**, answered via `tasks/update` — so background elicitation looks identical to foreground elicitation, with zero new client API.
|
||||
- **`call_tool_mcp(...)` (low-level)** — hands back the raw `CreateTaskResult` claimed shape for callers managing the task themselves.
|
||||
- **A "return quickly" flag on the friendly interface** yields the `Task` handle (`.status()`, `.wait()`, `.cancel()`, awaitable) without blocking — the escape hatch for progress and cancellation.
|
||||
|
||||
Server-side, `TaskConfig` modes translate directly: `required` → always task (`-32003` for non-declaring clients), `optional` → task iff the client declared, `forbidden` → never.
|
||||
|
||||
## Sequencing
|
||||
|
||||
1. **Design + unit-test the extension API** against tasks' full surface (capability, methods, interception, client claims/notifications) — as its own testable layer, proven in isolation with a trivial in-test extension before any tasks logic lands on it.
|
||||
2. **Build `fastmcp-tasks`** — extract the engine from the removed SEP-1686 layer, write the SEP-2663 adapter, port the client half.
|
||||
3. **Migrate MCP Apps onto the extension API** — fast-follow, off the critical path, with Apps' existing green tests as the regression net.
|
||||
|
||||
Tasks leads because only it exercises the full API surface; leading with the Apps subset would design us into a corner. Apps becomes the second consumer that confirms generality.
|
||||
|
||||
## Scope for v1 (non-goals)
|
||||
|
||||
- **Polling only.** The optional `notifications/tasks` push and `subscriptions/listen` integration are deferred to a later `fastmcp-tasks` version. This lets the second Redis notification queue die rather than be ported.
|
||||
- **`tools/call` only — do not lead the spec.** SEP-2663 augments `tools/call` only. FastMCP 3 offered `task=True` on prompts and resources *ahead* of the SDK under SEP-1686, and that was a mistake: it produced wire-inexpressible capability, a permanent xfail cluster, and the sdk-feedback #3 gap. The rebuild does **not** repeat it — `task=` is a tools-only surface, and the generic prompt/resource task spine is dropped rather than carried. If the spec extends augmentation later, the surface grows with it.
|
||||
- **Ship experimental.** The `ext-tasks` schema is labeled experimental with no releases; `fastmcp-tasks` ships labeled experimental initially and revs on its own cadence when the schema moves.
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Mitigation |
|
||||
| --- | --- |
|
||||
| **Spec churn** (extension is experimental) | Thin wire adapter over a wire-agnostic engine; ship experimental; SEP itself is Final, so the polling model is stable even if field names move. |
|
||||
| **Era gating** — SDK strips `capabilities.extensions` at pre-2026 negotiated versions (sdk-feedback #2) | Advertisement effectively requires the 2026-07-28 era. FastMCP 3 covers legacy tasks. **#2 now gates a flagship feature → escalate upstream.** |
|
||||
| **Co-developing a new abstraction + greenfield feature** | Build and unit-test the extension API in isolation first (step 1) before tasks logic lands on it. |
|
||||
| **Naming confusion** — `[tasks]` extra re-points under the same name | Deliberate changelog note; user code and the extra name are unchanged, only the wire modernizes. |
|
||||
|
||||
## Design decisions (resolved)
|
||||
|
||||
These were the open forks; the maintainer has settled them. Recorded here so the direction is unambiguous going into implementation.
|
||||
|
||||
1. **Wire adapter location — in the `fastmcp-tasks` package.** The engine *and* the SEP-2663 wire adapter live in the package; core carries only the `task=True` declaration. This isolates the experimental schema's churn from core, at the cost of diverging from the Apps precedent (where the `ui` wire glue lives in core today — Apps will converge onto this model when it migrates to the extension API).
|
||||
2. **Extension API shape — a FastMCP-native `mcp.add_extension()`, required to enable tasks.** Chosen over a thin pass-through to the SDK's `MCPServer(extensions=...)` because the FastMCP-native API can hand extensions the `Context`, component registry, and auth scope the SDK's `Extension` withholds. `add_extension` is **required** for `task=True` (not autodetected) — it is the single home for backend config and the honest source of capability advertisement. See [The extension API](#the-extension-api).
|
||||
3. **Client default — transparent completion on the friendly interface.** `call_tool` drives the poll loop and returns the finished result; `call_tool_mcp` exposes the raw `CreateTaskResult`; a "return quickly" flag yields the `Task` handle. See [Client experience](#client-experience).
|
||||
4. **Experimental labeling — yes.** `fastmcp-tasks` ships labeled experimental for at least one minor cycle, tracking the experimental `ext-tasks` schema.
|
||||
5. **Resource/prompt spine — dropped; tools-only.** The rebuild does not lead the SDK on augmentable request types, correcting the SEP-1686-era mistake. See [Scope for v1](#scope-for-v1-non-goals).
|
||||
559
docs/development/v4-notes/change-register.mdx
Normal file
559
docs/development/v4-notes/change-register.mdx
Normal file
|
|
@ -0,0 +1,559 @@
|
|||
---
|
||||
title: Change Register
|
||||
---
|
||||
|
||||
This is the complete register of user-facing changes from the MCP Python SDK v2 migration ([PR #4437](https://github.com/PrefectHQ/fastmcp/pull/4437)), organized by subsystem. It doubles as a review lens: take one subsystem, read its claimed changes, and verify each against the diff.
|
||||
|
||||
Each entry is tagged **Absorbed** (public surface unchanged), **Bridged** (shim keeps old code working, usually warning), **Breaking** (user code must change), or **Deprecated** (works, warns, slated for removal). See the [overview](/development/v4-notes/index) for what each disposition means.
|
||||
|
||||
**Empirical validation (WS2 upgrade reality-check).** The register's compatibility claims are verified, not predicted. Running unchanged 3.x-era code against this branch, all 11 upgrade scenarios pass or warn — the only failures are the two predicted breaks, user `mcp.types` imports and positional `McpError(ErrorData(...))` construction. Cross-version wire interop between a 3.4.3 peer and this branch is bidirectionally clean across 9 operations (3.4.3 client ↔ v4 server and v4 client ↔ 3.4.3 server over HTTP). All 29 `_ALIASES` bridge entries warn correctly with actionable messages.
|
||||
|
||||
## Environment
|
||||
|
||||
### Dependency floors: pydantic >= 2.12, Starlette >= 1.0 — Breaking (environment)
|
||||
|
||||
The SDK v2 raises FastMCP's dependency floors. Projects pinning an older pydantic (e.g. `2.11.*`) hit an unsatisfiable-resolution error at install time and must bump their pin; unpinned projects get pydantic upgraded silently. The server extra floors Starlette at `>=1.0.1` — modern FastAPI (0.11x+) already runs Starlette 1.x, so coexistence is clean (verified with FastAPI 0.138.2); only very old FastAPI pinned below Starlette 1.0 conflicts. Both are documented in the [upgrade guide's Environment requirements](/getting-started/upgrading/from-fastmcp-3#environment-requirements).
|
||||
|
||||
*Verify:* `fastmcp_slim/pyproject.toml` (`pydantic[email]>=2.12.0` core, `starlette>=1.0.1` server extra); WS2 environment-upgrade scenario.
|
||||
|
||||
## Types and imports
|
||||
|
||||
The SDK v2 split protocol types into a standalone `mcp_types` package and renamed every field from camelCase to snake_case. This is the single largest source of user-facing change, and FastMCP absorbs nearly all of it.
|
||||
|
||||
### `mcp.types` split into `mcp_types` — Breaking (by omission)
|
||||
|
||||
The `mcp.types` module no longer exists. Any `from mcp.types import X` or `import mcp.types` in user code raises `ImportError`. This is the one import change users cannot avoid.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/types.py`, and grep the diff for the doc migration `from mcp.types import` → `from fastmcp.types import` (30 sites).
|
||||
|
||||
### `fastmcp.types` is the stable home — Bridged
|
||||
|
||||
<Note>
|
||||
Superseded before release — see "`fastmcp.types` trimmed to FastMCP-unique types only" below. This section documents the re-export set as it existed mid-migration; none of it ever shipped.
|
||||
</Note>
|
||||
|
||||
FastMCP re-exports the protocol types users are most likely to touch from `fastmcp.types`, sourced from `mcp_types` (the `mcp` root package lacks most of them):
|
||||
|
||||
```python test="skip"
|
||||
from fastmcp.types import TextContent, Tool, ToolAnnotations, ErrorData
|
||||
```
|
||||
|
||||
The re-export set is deliberately limited to names that trace to a documented user import: `TextContent`, `ImageContent`, `AudioContent`, `EmbeddedResource`, `ResourceLink`, `ContentBlock`, `Tool`, `Resource`, `ResourceTemplate`, `Prompt`, `PromptMessage`, `CallToolResult`, `GetPromptResult`, `ReadResourceResult`, `TextResourceContents`, `BlobResourceContents`, `SamplingMessage`, `CreateMessageResult`, `SamplingCapability`, `Root`, `ErrorData`, `Completion`, `Annotations`, `ToolAnnotations`, `Icon`, `ToolResultContent`, plus the pre-existing `Textarea`. Notification and request wrapper types (e.g. `ToolListChangedNotification`) are not re-exported — import those from `mcp_types` directly.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/types.py` `__all__`.
|
||||
|
||||
### `fastmcp.types` trimmed to FastMCP-unique types only — Absorbed (post-review cleanup)
|
||||
|
||||
The re-export set above never shipped in a release, so it was cut before 4.0 rather than deprecated. `fastmcp.types` now holds only types FastMCP defines itself — `Textarea` — and every bare `mcp_types` mirror (`TextContent`, `Tool`, `ToolAnnotations`, `ErrorData`, and the rest of the 29-name list) is gone. Code that imported those from `fastmcp.types` now imports them from `mcp_types` directly:
|
||||
|
||||
```python
|
||||
from mcp_types import TextContent, Tool, ToolAnnotations, ErrorData
|
||||
```
|
||||
|
||||
Because `fastmcp.types.__all__` was `["Textarea"]` as of the last stable release (v3.4.4) and the mirrors were added only in this unreleased migration work, removing them breaks no released user — there is no bridge or deprecation warning to write.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/types.py` `__all__` (back down to `["Textarea"]`).
|
||||
|
||||
### camelCase field reads are bridged — Bridged (deprecated)
|
||||
|
||||
Objects FastMCP hands back — results of `client.list_tools()`, `client.call_tool_mcp()`, `client.read_resource()`, and the parameter objects passed to sampling and elicitation handlers — are SDK v2 objects with snake_case fields. A compatibility bridge installed at import time routes the old camelCase names to their snake_case fields, warning once per read:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
|
||||
async def read_schema():
|
||||
async with Client("my_mcp_server.py") as client:
|
||||
tools = await client.list_tools()
|
||||
return tools[0].inputSchema # works, warns; prefer .input_schema
|
||||
```
|
||||
|
||||
The bridged fields are exactly those users read, data-driven from an `_ALIASES` table: `inputSchema`/`outputSchema` (Tool); `readOnlyHint`/`destructiveHint`/`idempotentHint`/`openWorldHint` (ToolAnnotations); `mimeType` (Resource, ResourceTemplate, TextResourceContents, BlobResourceContents, ImageContent, AudioContent) and `uriTemplate` (ResourceTemplate); `isError`/`structuredContent` (CallToolResult); `hasMore` (Completion); `serverInfo`/`protocolVersion` (InitializeResult); `nextCursor`/`resourceTemplates` (List\*Result); `systemPrompt`/`maxTokens`/`stopSequences`/`modelPreferences`/`toolChoice` (CreateMessageRequestParams); `requestedSchema` (ElicitRequestFormParams). WS2 verified all 29 alias entries warn correctly with actionable messages.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/_compat.py` (the `_ALIASES` table and `install()`).
|
||||
|
||||
### The bridge is a genuine runtime toggle — Absorbed (post-review fix)
|
||||
|
||||
The bridge properties install unconditionally, and each getter reads the live `mcp_camelcase_compat` setting on every access: warn-and-return when enabled, raise `AttributeError` when disabled. An earlier version installed the bridge once at import, so flipping the setting afterward did nothing — commit `d9659453` fixed this so the toggle works at runtime:
|
||||
|
||||
```python
|
||||
import fastmcp
|
||||
|
||||
fastmcp.settings.mcp_camelcase_compat = False # now takes effect immediately
|
||||
```
|
||||
|
||||
The setting is documented in [Settings](/more/settings) as `FASTMCP_MCP_CAMELCASE_COMPAT`.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/settings.py` (setting), `fastmcp_slim/fastmcp/_compat.py` (per-read gate), commit `d9659453`.
|
||||
|
||||
### `mcp-types` is now a core slim dependency — Absorbed (post-review fix)
|
||||
|
||||
Bare `import fastmcp` loads `mcp_types` via `_sdk_patches` and `_compat`, so a bare `fastmcp-slim` install (without the `[mcp]` extra) hit `ModuleNotFoundError`. Because `mcp-types` only pulls `pydantic` and `typing-extensions` (already core), it was promoted to a core dependency while the full `mcp` SDK stays in the `[mcp]` extra.
|
||||
|
||||
*Verify:* `fastmcp_slim/pyproject.toml` (`mcp-types==2.0.0b1` in core dependencies), commit `e16ffad4`.
|
||||
|
||||
### `McpError` is an alias; construction changed — Bridged (catch) / Breaking (construct)
|
||||
|
||||
`fastmcp.exceptions.McpError` is a plain alias of the SDK's `MCPError` — a plain alias, not a subclass, so `except McpError` still catches SDK-raised errors and `err.error.code` still reads:
|
||||
|
||||
```python
|
||||
from fastmcp.exceptions import McpError
|
||||
|
||||
try:
|
||||
...
|
||||
except McpError as err:
|
||||
print(err.error.code) # unchanged
|
||||
```
|
||||
|
||||
Construction is the one unavoidable behavior break. The v1 pattern of wrapping an `ErrorData` positionally raises `TypeError` under v2; construct with keywords instead:
|
||||
|
||||
```python
|
||||
from fastmcp.exceptions import McpError
|
||||
|
||||
# Before (raises TypeError under SDK v2):
|
||||
# raise McpError(ErrorData(code=-32000, message="Client not supported"))
|
||||
|
||||
raise McpError(code=-32000, message="Client not supported")
|
||||
```
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/exceptions.py` (`McpError = MCPError`).
|
||||
|
||||
## Server core
|
||||
|
||||
The SDK v2 rewrote the server request-handling model. FastMCP's handler layer is the most heavily rewritten part of the migration, but the public server API is unchanged.
|
||||
|
||||
### Handler adapters — Absorbed
|
||||
|
||||
Handlers are now registered by method string via `add_request_handler(method, params_type, handler)`, take a uniform `(ctx, params)` signature, and return the **bare** result model (no `ServerResult` wrapper). FastMCP's `_setup_handlers` builds one thin adapter per method (`tools/list`, `tools/call`, `resources/read`, `prompts/get`, `logging/setLevel`, …) that binds the request context, adapts params to the existing handler body, and returns the bare result. The v1 decorator overrides and `_wrap_list_handler` are deleted.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/low_level.py` (462 lines changed), `fastmcp_slim/fastmcp/server/mixins/mcp_operations.py`.
|
||||
|
||||
### FastMCP-owned request context — Absorbed
|
||||
|
||||
The SDK's `request_ctx` ContextVar is gone; the SDK passes context to handlers as an argument only. FastMCP owns its own `fastmcp_request_ctx` ContextVar, set at the top of every adapter. It stores a FastMCP-owned `FastMCPRequestContext` wrapper rather than the raw SDK context, because the raw `ServerRequestContext.meta` is a bare `TypedDict` carrying only `progress_token` — the full `_meta` block (which holds `_meta.fastmcp.version` and the distributed-trace parent) has to be lifted out of the raw params dict. `Context.request_context` and its consumers (`report_progress`, `session_id`, telemetry trace extraction, `get_http_request`) all read through the wrapper.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/dependencies.py`, `server/context.py`, `server/telemetry.py`.
|
||||
|
||||
### `ServerMiddleware` bridge for `initialize` — Absorbed
|
||||
|
||||
Server-side middleware is a new first-class SDK concept: `Server.middleware` is a list of `ServerMiddleware` composed around every request and notification, including `initialize`. FastMCP no longer subclasses `ServerSession` (the runner constructs it), so the old `MiddlewareServerSession._received_request` override is gone. A `FastMCPServerMiddleware` is appended to the SDK's middleware list (preserving the SDK's own OpenTelemetry middleware) and intercepts `initialize` to run FastMCP's middleware chain. The v2 interface is cleaner — `call_next(ctx)` returns the serialized result directly, so the old `capturing_respond` machinery is deleted.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/low_level.py` (`FastMCPServerMiddleware`).
|
||||
|
||||
### Middleware observes every inbound message — New (coverage)
|
||||
|
||||
FastMCP's `Middleware` chain used to begin *inside* the per-method handlers, so `on_message`/`on_request`/`on_notification` only fired for messages that reached a tool/resource/prompt handler. Notifications, cancellations, and malformed or unroutable requests were invisible to middleware. `FastMCPServerMiddleware` — FastMCP's entry in the SDK's own middleware list — is now the dispatch root: it runs the `on_message`/`on_request`/`on_notification` pass for every message the interior handlers do not dispatch (all notifications including `notifications/cancelled`, `ping`, `logging/setLevel`, unknown methods, and component requests that fail validation before the handler runs). The component methods keep their interior dispatch unchanged, so `on_call_tool` and friends still receive the typed component result and a tool exception still propagates through `on_message`/`on_request` exactly where the built-in error/logging/timing middleware expect it — each hook fires exactly once per message. Multi-round (SEP-2322) calls compose cleanly with this: each round is a complete request→response cycle through the full chain, and an asking round's `call_next` returns the ask as an ordinary `InputRequiredToolResult` value (see the MRTR entry below). All thirteen built-in middleware pass their suites unmodified. See [What middleware sees](/servers/middleware#what-middleware-sees).
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/low_level.py` (`FastMCPServerMiddleware` root dispatch, `_INTERIOR_METHODS`), `fastmcp_slim/fastmcp/server/middleware/middleware.py` (`MiddlewarePhase`, `mark_interior_dispatched`), `fastmcp_slim/fastmcp/server/server.py` (`_dispatch_component_middleware`), `tests/server/middleware/test_message_visibility.py`.
|
||||
|
||||
### Per-session state re-homed to the connection — Absorbed
|
||||
|
||||
Because `ServerSession` is now per-request, per-session state can no longer live on the session object. The minimum logging level is re-homed to a FastMCP-side map keyed by session id (via `connection.session_id`), and `client_supports_extension` becomes a free function reading `session.client_params.capabilities`.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/low_level.py`, `server/context.py` (`_log_to_server_and_client`).
|
||||
|
||||
### `extensions` capability read from the real field — Absorbed (post-review fix)
|
||||
|
||||
SDK v2 declares `extensions` as a real field on `ClientCapabilities`, so a client sending `ClientCapabilities(extensions={...})` populates the field, not `model_extra`. `client_supports_extension` now reads `caps.extensions` first and falls back to `model_extra` only for legacy-serialized clients.
|
||||
|
||||
*Verify:* commit `96ca0092`, `server/low_level.py` / `server/context.py`.
|
||||
|
||||
### Task protocol and the `_sdk_patches` shim — Absorbed (with an upstream gap)
|
||||
|
||||
The SEP-1686 task CRUD protocol (`tasks/get`, `tasks/result`, `tasks/list`, `tasks/cancel`) is entirely FastMCP-owned — the SDK ships no task store. Task detection moves to a params field: `params.task is not None` on `CallToolRequestParams`, with `ttl` from `params.task.ttl`. The four task handlers port to `add_request_handler`.
|
||||
|
||||
The SDK has a real gap here (see [Known Gaps](/development/v4-notes/known-gaps) and sdk-feedback #1): it ships the task result types but omits them from the method registries, so a background-task `tools/call` returning a `CreateTaskResult` fails validation. FastMCP installs a registry-widening shim in `_sdk_patches.py` that adds `CreateTaskResult` to the `tools/call` result union and registers the `tasks/*` rows. It is a temporary patch with a self-documented removal trigger.
|
||||
|
||||
Resources and prompts have **no `task` field** on their params in b1, so task-augmented resource reads and prompt gets are not wire-expressible — a documented capability regression, tracked by xfails, not a bug FastMCP fixes.
|
||||
|
||||
This section records the migration's *handling* of the SEP-1686 wire layer as it stood at merge. That layer is not the end state: it is slated for removal and rebuild on the `io.modelcontextprotocol/tasks` extension (SEP-2663) as the `fastmcp-tasks` package. See [Background Tasks (SEP-2663)](/development/v4-notes/background-tasks) for the forward plan; the `_sdk_patches.py` shim and the `server/tasks/*` wire handlers described here go away with it, while the Docket execution engine moves into `fastmcp-tasks`.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/_sdk_patches.py`, `server/tasks/*`.
|
||||
|
||||
### Single SERVER span per request — Absorbed (post-migration fix)
|
||||
|
||||
SDK v2 seeds an `OpenTelemetryMiddleware` into every lowlevel `Server`, so each inbound request already emits a SERVER span. FastMCP emits its own richer SERVER span per request (with `fastmcp.*` and auth/session attributes), so a server with an OTel exporter installed would export **two** SERVER spans per request under different attribute conventions. `LowLevelServer.__init__` now drops the SDK's seeded `OpenTelemetryMiddleware` (matched by type, not position, leaving any other seeded middleware intact) and keeps FastMCP's spans. Inbound W3C trace-context extraction is unaffected — FastMCP's telemetry reads `traceparent` from `_meta` itself, so distributed traces still link client to server. Client-side is not double-counted: the SDK's `ClientSession` emits a low-level `MCP send <method>` CLIENT span that nests *under* FastMCP's high-level client span, a legitimate parent/child hierarchy rather than a duplicate.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/low_level.py` (the `OpenTelemetryMiddleware` filter); `tests/server/telemetry/test_server_tracing.py::TestSingleServerSpan`.
|
||||
|
||||
### Telemetry on by default, with an explicit off-switch — Absorbed
|
||||
|
||||
FastMCP's OpenTelemetry instrumentation is on by default. Because FastMCP uses only the OpenTelemetry API, span creation is a no-op with negligible overhead (the API's `NonRecordingSpan`) unless the user configures an SDK and exporter — so being always-on costs nothing until you opt into collection. The new `FASTMCP_ENABLE_TELEMETRY` setting (`fastmcp.settings.enable_telemetry`, default `true`) is the explicit off-switch: set it to `false` and `get_tracer()` returns a genuine no-op tracer, so no FastMCP spans are created even when an SDK is configured. The off-switch governs FastMCP's own spans (all SERVER spans, plus FastMCP's high-level CLIENT span); the SDK's low-level `mcp-python-sdk` `MCP send <method>` CLIENT spans are governed by the user's OpenTelemetry SDK, not this setting. FastMCP's SERVER span now also carries `mcp.protocol.version` — the attribute the dropped SDK `OpenTelemetryMiddleware` set — restoring parity with the SDK's semantic conventions.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/settings.py` (`enable_telemetry`); `fastmcp_slim/fastmcp/telemetry.py` (`get_tracer` off-switch); `fastmcp_slim/fastmcp/server/telemetry.py` (`get_protocol_span_attributes`); `tests/server/telemetry/test_server_tracing.py::TestTelemetryEnabledByDefault`, `::TestProtocolVersionAttribute`.
|
||||
|
||||
### Spec-correct error codes via a central translator — Breaking (wire error code)
|
||||
|
||||
Resource-not-found responses from the core `resources/read` handler previously used `-32002`. SEP-2164 (and the SDK's own mcpserver, which maps `ResourceNotFoundError` → `INVALID_PARAMS`) makes this `-32602`. The per-adapter `MCPError(code=..., ...)` literals in `server/mixins/mcp_operations.py` are replaced by a single `fastmcp.exceptions.to_mcp_error()` translator that maps FastMCP's public exceptions to the `mcp_types` code constants (`NotFoundError`/`DisabledError`/`ValidationError` → `INVALID_PARAMS`, else `INTERNAL_ERROR`). Clients that string-matched on the old `-32002` for resource-not-found must switch to `-32602`; the human-readable message ("Resource not found: ...") is unchanged. The opt-in `ErrorHandlingMiddleware`, which has its own documented per-method-prefix code mapping, is intentionally left as-is.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/exceptions.py` (`to_mcp_error`); `fastmcp_slim/fastmcp/server/mixins/mcp_operations.py`; `tests/test_exceptions.py`.
|
||||
|
||||
### `Cachable*` response-cache models renamed to `Cacheable*` — Breaking (rename) <!-- codespell:ignore -->
|
||||
|
||||
The response-caching middleware's Pydantic wrapper models — used to serialize cached tool, resource, and prompt results for `ResponseCachingMiddleware` — carried a spelling typo. `CachableToolResult`, `CachableResourceContent`, `CachableResourceResult`, `CachableMessage`, and `CachablePromptResult` are renamed to `CacheableToolResult`, `CacheableResourceContent`, `CacheableResourceResult`, `CacheableMessage`, and `CacheablePromptResult`. None of these classes are re-exported from `fastmcp` or any package `__init__.py`, so the realistic blast radius is limited to code that imported the old names directly from `fastmcp.server.middleware.caching`:
|
||||
|
||||
```python
|
||||
# Before (now raises ImportError):
|
||||
# from fastmcp.server.middleware.caching import CachableToolResult
|
||||
|
||||
# After
|
||||
from fastmcp.server.middleware.caching import CacheableToolResult
|
||||
```
|
||||
|
||||
There is deliberately no compatibility alias for the old spelling.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/middleware/caching.py`.
|
||||
|
||||
### Server-side argument completion — New (opt-in feature)
|
||||
|
||||
A FastMCP server can now answer `completion/complete` requests, suggesting values for prompt arguments and resource-template parameters as a user types. Previously a FastMCP *client* could call `complete()` but a FastMCP *server* had no way to respond — the method was unregistered, so it returned `-32601` (method-not-found) on both eras. The new `@mcp.completion` decorator registers a single server-level handler that receives the reference (a `PromptReference` or `ResourceTemplateReference`), the `CompletionArgument` being completed, and the optional `CompletionContext` of already-supplied argument values, and returns candidates — a list of strings, a `Completion` (to carry the `total`/`has_more` pagination hints), or `None`/empty for a reference it does not recognize (which yields an empty completion, not an error).
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from mcp_types import PromptReference
|
||||
|
||||
mcp = FastMCP("Completion Server")
|
||||
|
||||
|
||||
@mcp.prompt
|
||||
def write_poem(theme: str) -> str:
|
||||
return f"Write a poem about {theme}"
|
||||
|
||||
|
||||
@mcp.completion
|
||||
def complete(ref, argument, context):
|
||||
if isinstance(ref, PromptReference) and argument.name == "theme":
|
||||
options = ["nature", "love", "adventure"]
|
||||
return [o for o in options if o.startswith(argument.value)]
|
||||
return None
|
||||
```
|
||||
|
||||
The completions capability is declared exactly when a handler exists: `add_completion_handler` registers the low-level `completion/complete` handler, and the SDK derives the capability from that handler's presence — a server with no completion handler does not advertise it. FastMCP does not hand-set the capability. The single-handler shape mirrors the SDK's own `completion/complete` surface and FastMCP's existing client-side `Client.complete()`, and it slots into the `@mcp.tool`/`@mcp.prompt`/`@mcp.resource` decorator lineup as another server-level `@mcp.<verb>` registration rather than inventing a per-argument sub-decorator idiom. It works identically on the handshake and modern (`2026-07-28`) eras, since `completion/complete` is a request/response method that flows on every era. The authoring types — `PromptReference`, `ResourceTemplateReference`, `CompletionArgument`, `CompletionContext`, and `Completion` — are imported from `mcp_types`, not `fastmcp.types`.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/completions.py` (handler type + `normalize_completion`), `fastmcp_slim/fastmcp/server/server.py` (`completion` decorator, `add_completion_handler`), `fastmcp_slim/fastmcp/server/mixins/mcp_operations.py` (`_on_complete`), `tests/server/test_completions.py`, `docs/servers/completions.mdx`.
|
||||
|
||||
## Client
|
||||
|
||||
The `fastmcp.Client` public API is largely preserved. The client stays a wrapper around `mcp.ClientSession`; the first-class `mcp.client.Client` is deliberately not adopted. Two client-surface changes are called out below: the connection `mode` default flips to `"auto"`, and `extensions=` / `result_claims=` are newly surfaced.
|
||||
|
||||
### Connection `mode` defaults to `"auto"` — Breaking (behavior)
|
||||
|
||||
`Client(mode=...)` now defaults to `"auto"` instead of `"legacy"`. The client probes `server/discover` and adopts the modern (`2026-07-28`) era when the server responds, denylist-falling-back to the initialize handshake for any server that is not positive evidence of a modern peer. Against a FastMCP server (which serves both eras), an ordinary `Client(url)` now negotiates the modern era by default, where the legacy-only Context push features are unavailable per the per-feature era matrix (see the *Protocol eras* section below) — server-initiated sampling/elicitation/roots, `ping`, session ids, and FastMCP task submission all require the legacy era. The one-line revert is `Client(..., mode="legacy")`, which restores byte-identical pre-v4 negotiation.
|
||||
|
||||
The SSE transport is legacy-only (it cannot carry the sessionless modern era), so a client connecting over SSE negotiates the legacy handshake even under `mode="auto"` — expressed by a `ClientTransport.legacy_only` flag set on `SSETransport`. `MCPConfigTransport` reports `legacy_only` as a property: a multi-server config is legacy-only (each backend is mounted behind a legacy-era proxy), while a single-server config mirrors its one backend transport's era so a modern Streamable HTTP backend stays modern-capable. Two internal library clients that are inherently handshake-based are pinned to legacy so the flip does not break them: the `ProxyClient` backend (which forwards the initialize handshake and server-initiated features) defaults to `mode="legacy"`, and the `inspect` utility (which reads the full `server_info` only the handshake carries) connects legacy.
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
client = Client("https://example.com/mcp") # now negotiates "auto"
|
||||
client = Client("https://example.com/mcp", mode="legacy") # opt back into the handshake
|
||||
```
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/client/client.py` (`mode` default, `_negotiate` `legacy_only` shortcut), `fastmcp_slim/fastmcp/client/transports/{base,sse,config}.py` (`legacy_only`), `fastmcp_slim/fastmcp/server/providers/proxy.py` (`ProxyClient` legacy default), `fastmcp_slim/fastmcp/mcp_config.py` and `fastmcp_slim/fastmcp/utilities/inspect.py` (legacy inner clients), `tests/client/client/test_mode_negotiation.py` (default, clean discover-rejection fallback, legacy-only transport), `tests/test_mcp_config.py` (single- vs multi-server `legacy_only`), `docs/clients/client.mdx`.
|
||||
|
||||
### `extensions=` / `result_claims=` surfaced — New (opt-in feature)
|
||||
|
||||
`fastmcp.Client` now accepts `extensions=` (a sequence of SEP-2133 `ClientExtension` instances) and `result_claims=` (extra `ResultClaim`s keyed by an advertised extension's identifier). Each extension's capability advertisement, result claims, and notification bindings are folded into the underlying `ClientSession` on every transport. User-supplied notification bindings **compose** with FastMCP's internal task-status binding rather than clobbering it: the task binding always leads, and a user extension that binds the same method surfaces a clear duplicate-method error at connect time rather than silently winning. Result claims are wired end-to-end: `call_tool()` / `call_tool_mcp()` pass `allow_claimed=True` and resolve a claimed result through the owning claim's resolver (`ClaimContext`), so a server-emitted claimed shape is finished into an ordinary `CallToolResult` instead of raising `UnexpectedClaimedResult`. Claimed shapes are modern-only, so they are inert on a legacy connection.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/client/client.py` (`_build_extension_kwargs`, `_resolve_claimed_result`, `new()`), `fastmcp_slim/fastmcp/client/mixins/tools.py` (`call_tool_mcp` claim resolution), `fastmcp_slim/fastmcp/client/transports/base.py` (`SessionKwargs.extensions`/`result_claims`), `tests/client/test_client_extensions.py` (fold, composition, live both-bindings-fire, end-to-end claim resolution).
|
||||
|
||||
### Protocol helpers delegated to the SDK — Absorbed (internal)
|
||||
|
||||
`fastmcp.Client` carried forked copies of three SDK helpers — `_fold_extensions` (with its `_FoldedExtensions` dataclass), `_evicting_message_handler`, and `_synthesize_discover` — written when the SDK had not yet stabilized them. It now imports the SDK's implementations directly. The forks had already drifted: FastMCP's `_fold_extensions` was missing the SEP-2133 `validate_extension_identifier` check, so a non-reverse-DNS extension identifier that the SDK rejects was silently accepted. Adopting the SDK's version closes that gap. No public surface moves; the SDK returns `None` rather than empty collections for the folded claims and bindings, absorbed at the two call sites in `_build_extension_kwargs`.
|
||||
|
||||
Full composition — `fastmcp.Client` holding an `mcp.Client` and delegating the connection lifecycle to it — remains blocked upstream. `mcp.Client._build_session` hardcodes `ClientSession(...)` with no override hook, but FastMCP's `TransportOptions.session_class` is load-bearing: `ProxyClient` supplies a `_ForwardingClientSession` that skips output-schema validation so a backend's schema bug surfaces at the end client rather than as a proxy error. Separately, `mcp.Client.__aenter__` raises on reentry, while FastMCP's refcounted reentrant context manager is depended on by proxy session reuse. Both would need an upstream `session_factory=` hook (the same shape as the `notification_bindings=` ask that unblocked extension composition) before the lifecycle itself can be delegated.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/client/client.py` (imports from `mcp.client.client`; no local helper definitions), `fastmcp_slim/fastmcp/client/transports/base.py` (`TransportOptions.session_class`), `fastmcp_slim/fastmcp/server/providers/proxy.py` (`_ForwardingClientSession`, `PROXY_TRANSPORT_OPTIONS`).
|
||||
|
||||
### Transports yield 2-tuples — Absorbed
|
||||
|
||||
All SDK transports (`streamable_http_client`, `sse_client`, `stdio_client`) now yield a 2-tuple `(read, write)` instead of exposing a third `get_session_id` element. HTTP configuration flows through a caller-supplied `http_client=`. Only the tuple unpack changed on the FastMCP side.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/client/transports/http.py`, `transports/sse.py`, `transports/stdio.py`.
|
||||
|
||||
### Float timeouts; `timedelta` still accepted — Absorbed
|
||||
|
||||
The SDK session and call timeouts are now plain floats. FastMCP's public `Client(timeout=...)` still accepts a `timedelta`, a plain float, or an int, normalizing through the existing `normalize_timeout_to_seconds` at the `SessionKwargs` chokepoint:
|
||||
|
||||
```python
|
||||
from datetime import timedelta
|
||||
|
||||
from fastmcp import Client
|
||||
|
||||
client = Client("my_mcp_server.py", timeout=timedelta(seconds=30)) # still works
|
||||
client = Client("my_mcp_server.py", timeout=30.0) # also works
|
||||
```
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/client/transports/base.py` (`SessionKwargs.read_timeout_seconds: float | None`), `client/client.py`.
|
||||
|
||||
### Connection settings passed to `connect_session` — Breaking (custom transports)
|
||||
|
||||
`ClientTransport.connect_session` takes a new keyword-only `transport_options: TransportOptions | None`, describing how the connecting client wants its session built: which `ClientSession` class to instantiate, and whether to forward the caller's authorization header upstream. Proxies use it to relay backend results without enforcing their output schema (see [Proxy Servers](/servers/providers/proxy#tool-results-are-relayed-not-inspected)).
|
||||
|
||||
These settings previously lived on the transport instance, so a transport shared between clients leaked one client's configuration into another — including credential forwarding, which `create_proxy(some_client)` would silently enable on the caller's own client. They now travel with the client that wants them, and `forward_incoming_headers` is no longer a settable transport attribute.
|
||||
|
||||
A client only passes the argument when it wants non-default settings, so an ordinary `Client` is unaffected and transports that don't accept it keep working. A custom `ClientTransport` used as a *proxy backend* must accept and honor it:
|
||||
|
||||
```python
|
||||
import contextlib
|
||||
|
||||
from fastmcp.client.transports.base import ClientTransport, TransportOptions
|
||||
|
||||
class MyTransport(ClientTransport):
|
||||
@contextlib.asynccontextmanager
|
||||
async def connect_session(self, *, transport_options=None, **session_kwargs):
|
||||
options = transport_options or TransportOptions()
|
||||
async with options.session_class(read, write, **session_kwargs) as session:
|
||||
yield session
|
||||
```
|
||||
|
||||
A transport that wraps others must pass it along; `MCPConfigTransport` forwards it to both its single-server delegate and its composite server.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/client/transports/base.py` (`TransportOptions`), the four built-in transports, `transports/config.py`, and `tests/server/providers/proxy/test_proxy_server.py`.
|
||||
|
||||
### `get_session_id` via header sniff — Bridged
|
||||
|
||||
The SDK dropped `get_session_id` from the streamable-HTTP transport with no replacement (the SDK source has an author TODO acknowledging it breaks the Transport protocol). FastMCP reconstructs it by registering an httpx2 response event hook on the client it owns, capturing the `mcp-session-id` response header (httpx2 preserves httpx's `event_hooks` API). The removal trigger is the upstream TODO.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/client/transports/http.py` (`_capture_session_id`, `get_session_id`).
|
||||
|
||||
### Pagination via `params=` — Absorbed
|
||||
|
||||
The SDK's `cursor=` kwarg on `list_*` is gone; pagination now flows through `params=PaginatedRequestParams(cursor=...)`. FastMCP's public `cursor=` on the `list_*_mcp` methods is preserved and translated internally.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/client/mixins/{tools,resources,prompts}.py`.
|
||||
|
||||
### OAuth `callback_handler` returns `AuthorizationCodeResult` — Breaking (advanced)
|
||||
|
||||
The one OAuth break: a custom `callback_handler` must return an `AuthorizationCodeResult` (fields `code`, `state`, `iss`) instead of the old `tuple[str, str | None]`. Everything else in the OAuth surface — `OAuthClientProvider` kwargs, `TokenStorage`, `async_auth_flow` — is unchanged.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/client/auth/oauth.py`.
|
||||
|
||||
### Notification dispatch unwrapped — Absorbed
|
||||
|
||||
The client's notification handling was reworked for the v2 message model. Custom server-to-client notifications (like SEP-1686 `notifications/tasks/status`) are no longer tee'd to a user `message_handler` — the SDK routes them only through `NotificationBinding` (see sdk-feedback #8). FastMCP registers a binding so task-status updates reach the Task registry.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/client/messages.py`, `client/tasks.py`.
|
||||
|
||||
### `SDKServer` alias — Absorbed (post-review rename)
|
||||
|
||||
The in-memory transport resolves the low-level server per server type. The alias for the SDK's own `MCPServer` was renamed from the misleading `FastMCP1Server` / `FastMCP1x` to `SDKServer`, since it names the SDK v2 server, not a FastMCP 1.x object.
|
||||
|
||||
*Verify:* commit `5c3b82e4`; `client/client.py`, `client/transports/memory.py`, `server/providers/proxy.py`, `cli/run.py`.
|
||||
|
||||
### Proxy request-context stash — Absorbed (post-review fix)
|
||||
|
||||
Proxy forwarding handlers stash the request context so a backend that issues a server-initiated request (list_roots/sampling/elicitation) can relay it back to the proxy's own client. This stash was initially applied only on the tool path; commit `1ac166bd` extended it to proxied resources, templates, and prompts.
|
||||
|
||||
*Verify:* commit `1ac166bd`, `server/providers/proxy.py`.
|
||||
|
||||
### Shared response cache via `KeyValueResponseCacheStore` — New
|
||||
|
||||
The SDK's client response cache (SEP-2549) reads and writes through a pluggable `ResponseCacheStore`; the default is a per-client in-memory LRU. FastMCP adds `KeyValueResponseCacheStore`, an adapter over the same `AsyncKeyValue` key-value abstraction the event store and OAuth proxy already use, so a fleet of clients (e.g. proxy replicas) can share one Redis-backed response cache. Pass it via `CacheConfig(store=...)`; a custom store requires an explicit `partition` (SDK) and `target_id` (FastMCP). Results serialize through a type-tagged envelope validated against an allowlist of cacheable result models — an unknown tag is a cache miss, never an import-by-name — and each adapter owns its own collection so `clear()` never touches another tenant.
|
||||
|
||||
```python
|
||||
from fastmcp.client.caching import KeyValueResponseCacheStore
|
||||
from mcp.client.caching import CacheConfig
|
||||
from key_value.aio.stores.redis import RedisStore
|
||||
|
||||
store = KeyValueResponseCacheStore(storage=RedisStore(url="redis://localhost"))
|
||||
config = CacheConfig(store=store, partition="tenant-a", target_id="weather-api")
|
||||
```
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/client/caching.py`, `tests/client/client/test_kv_response_cache.py`.
|
||||
|
||||
### Machine-to-machine client auth — New (feature)
|
||||
|
||||
`fastmcp.client.auth` gains two browser-free auth providers for the OAuth 2.0 `client_credentials` grant, closing the most common client-auth gap (previously only interactive `OAuth` and static `BearerAuth` were available). `ClientCredentialsOAuthProvider(client_id=..., client_secret=...)` authenticates with a client ID and secret; `PrivateKeyJWTOAuthProvider(client_id=..., assertion_provider=...)` uses an RFC 7523 `private_key_jwt` assertion (workload identity federation or a locally signed JWT via the re-exported `SignedJWTParameters` / `static_assertion_provider` helpers). Both are thin wrappers over the SDK's `mcp.client.auth.extensions.client_credentials` providers and implement `httpx2.Auth`, so they slot into the same `Client(auth=...)` path as every other provider. Like interactive `OAuth`, they take the MCP server URL (the token endpoint is discovered from OAuth metadata) and bind to it lazily — omit `mcp_url` and the transport supplies it. In-memory token storage is the default with no warning, since a lost M2M token is re-acquired in one non-interactive request.
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.auth import ClientCredentialsOAuthProvider
|
||||
|
||||
auth = ClientCredentialsOAuthProvider(client_id="id", client_secret="secret")
|
||||
async with Client("https://example.com/mcp", auth=auth) as client:
|
||||
await client.list_tools()
|
||||
```
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/client/auth/client_credentials.py`, `fastmcp_slim/fastmcp/client/transports/{http,sse}.py`, `tests/client/auth/test_client_credentials.py`.
|
||||
|
||||
## HTTP
|
||||
|
||||
The maintainer asked whether FastMCP can now delete its custom HTTP app and let the SDK's `Server.streamable_http_app()` handle everything. The answer for this PR is **no** — every override earns its keep. Convergence is a v4 project gated on three upstream additions (see [Feature Program](/development/v4-notes/feature-program)).
|
||||
|
||||
### Kept overrides — Absorbed
|
||||
|
||||
Four overrides survive, each for a concrete reason:
|
||||
|
||||
1. **Event-store session scoping.** The SDK hands every per-session transport the *same* `event_store` object, one stream-ID keyspace shared across sessions. FastMCP's `FastMCPStreamableHTTPSessionManager` returns a fresh `SessionScopedEventStore(shared, session_id=…)` per session, so resumability events don't leak across sessions.
|
||||
2. **Lifespan reconciliation.** The SDK builder enters the bare lowlevel `Server.lifespan` (which yields `{}`). FastMCP drives its own `_lifespan_manager` — ref-counted for mounts, Ctrl-C-shielded, docket-aware. The SDK path silently skips all of it, so FastMCP sets the server lifespan to delegate to `_lifespan_manager` and lets the manager enter it once.
|
||||
3. **Graceful transport termination.** FastMCP's lifespan `finally` drains the manager's server instances via `transport.terminate()` before task-group cancel, fixing the Uvicorn "returned without completing response" edge (#3025). The SDK just cancels.
|
||||
4. **User ASGI middleware hook.** The SDK builder hardcodes an empty middleware list and only appends auth. FastMCP's `http_app(middleware=...)` and `RequestContextMiddleware` have nowhere to go in the SDK path.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/http.py`, `server/event_store.py`, `server/mixins/lifespan.py`.
|
||||
|
||||
### DNS-rebinding ownership — Absorbed (security)
|
||||
|
||||
FastMCP owns DNS-rebinding protection through its `HostOriginGuardMiddleware`, which is more expressive than the SDK's and is the documented surface. To avoid two allowlists double-blocking with confusing errors from two layers, FastMCP **always** disables the SDK's layer by passing `TransportSecuritySettings(enable_dns_rebinding_protection=False)` to the manager — both when FastMCP's protection is on (so they don't double-block) and when it's off (so the SDK's default-on flip can't silently re-enable it).
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/http.py` (`enable_dns_rebinding_protection=False`, `HostOriginGuardMiddleware`).
|
||||
|
||||
### httpx2 replaces httpx — Breaking (custom client/factory, typing) / Absorbed (everything else)
|
||||
|
||||
SDK v2.0.0b2 replaces `httpx` + `httpx-sse` with [httpx2](https://pypi.org/project/httpx2/) (`>=2.5.0`), a next-generation httpx fork with built-in SSE. httpx2 is a near drop-in fork: the public API (`AsyncClient`, `Auth`, `Request`, `Response`, `Timeout`, `MockTransport`, exception hierarchy, `event_hooks`) matches httpx name-for-name. The SDK duck-types the client you hand it — `streamable_http_client(http_client=...)` and `sse_client(httpx_client_factory=...)` are type-hinted `httpx2.AsyncClient` with no `isinstance` gate — but the objects that cross into the SDK must be httpx2.
|
||||
|
||||
FastMCP now uses **httpx2 exclusively** and no longer depends on `httpx`. Every FastMCP-owned HTTP path moves to httpx2: the client transports (`client/transports/{base,http,sse}.py`), client auth (`client/auth/{oauth,bearer}.py` — `BearerAuth`/`OAuth` subclass `httpx2.Auth`), the client-side exception-group handler (`utilities/exceptions.py`), the proxy's upstream client (`server/providers/proxy.py`), the `MCPConfig` client-auth field (`mcp_config.py`), **and** all the server-side code that the earlier migration pass had left on httpx — the ~15 server auth providers' upstream IdP calls, the OpenAPI provider, `from_openapi`/`from_fastapi`, `version_check`, `resources/types.py`, the SSRF download guard, and the `apps_dev` CLI. `httpx` is dropped from the `mcp` extra entirely (it may still arrive transitively via other libraries, but FastMCP never imports it). The ~170 `httpx_mock` calls across the security-critical server-auth test files are ported to a local httpx2-backed `httpx_mock` fixture (`tests/utilities/httpx2_mock.py`) that preserves the `add_response`/`add_exception`/`get_request(s)` API verbatim, so `pytest-httpx` is dropped too.
|
||||
|
||||
User-visible deltas:
|
||||
|
||||
- **Custom client factory / client.** `StreamableHttpTransport(httpx_client_factory=...)`, `SSETransport(httpx_client_factory=...)`, and `OAuth(httpx_client_factory=...)` factories must now return `httpx2.AsyncClient`; a custom `httpx.Auth` passed as `Client(auth=...)` should become `httpx2.Auth`. httpx2 is a drop-in fork, so the change is an import swap (`import httpx` → `import httpx2`). This is a typing break; at runtime a duck-compatible httpx client still satisfies the SDK, but mixing `httpx.Timeout`/`httpx.Auth` with an httpx2 client is unsupported.
|
||||
- **OpenAPI client.** `FastMCP.from_openapi(client=...)` and `OpenAPIProvider(client=...)` are now type-hinted `httpx2.AsyncClient`. There is no `isinstance` gate, so an existing `httpx.AsyncClient` still works at runtime via duck-typing this release; the typing nudges you to httpx2.
|
||||
- **TLS trust store.** httpx2 verifies TLS against the OS trust store via `truststore` (honoring `SSL_CERT_FILE`/`SSL_CERT_DIR` first) instead of the bundled certifi CA set. This now applies to **all** FastMCP HTTP, including server-auth upstream IdP calls — not just the client path. Corporate-CA and certifi-pinned setups may see different trust behavior.
|
||||
- **Logger renames.** FastMCP HTTP now logs under `httpx2` and `httpcore2.*` (was `httpx`/`httpcore.*`). Anyone filtering FastMCP HTTP logs by logger name must update the names.
|
||||
|
||||
The session-id header hook (below) works unchanged: httpx2 keeps httpx's `event_hooks` API. FastMCP's tool/resource/prompt handlers still map upstream 429/timeout errors to actionable `ToolError`/`ResourceError`; because a user's own tool may raise from either library, `server/server.py` catches both `httpx2` and (if installed) legacy `httpx` `HTTPStatusError`/`TimeoutException` via a defensive `try: import httpx` shim.
|
||||
|
||||
*Verify:* `fastmcp_slim/pyproject.toml` (`mcp` extra lists only `httpx2`); no FastMCP source imports `httpx` except the documented defensive shim in `server/server.py`.
|
||||
|
||||
## Protocol eras
|
||||
|
||||
The SDK v2 serves multiple protocol eras from one server, and FastMCP formally embraces this.
|
||||
|
||||
### Dual-era serving — Absorbed (supersedes "latest only")
|
||||
|
||||
A single FastMCP server now handles clients across the protocol transition: the session-based handshake eras (through 2025-11-25) and the sessionless `2026-07-28` era (capability discovery via `server/discover`) simultaneously. This supersedes FastMCP's earlier "latest protocol only" stance.
|
||||
|
||||
### Per-feature era matrix — Breaking (feature availability by era)
|
||||
|
||||
The push-style Context features that require the server to call back into the client are unavailable on the sessionless `2026-07-28` era, because that era removes server-initiated requests (SEP-2577). The request/response features flow on every era.
|
||||
|
||||
| Context feature | Session-based eras | `2026-07-28` (sessionless) |
|
||||
| --- | --- | --- |
|
||||
| `ctx.info` / logging notifications | Supported | Supported |
|
||||
| Tools, resources, prompts, completions | Supported | Supported |
|
||||
| `ctx.elicit` (imperative) | Supported | Not on the back-channel — use [elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol) |
|
||||
| `ctx.sample` / `ctx.sample_step` | Supported (deprecated) | Removed — call an LLM server-side |
|
||||
| `ctx.list_roots` | Supported | Via the [guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol) |
|
||||
| Background tasks (`task=True`) | Runs synchronously — never tasked | Supported via the tasks extension |
|
||||
|
||||
Tools that rely on `ctx.elicit` or `ctx.list_roots` continue to work against clients on the session-based eras. On the modern era, elicitation is reachable through the multi-round "guard" pattern instead (a tool returns an `InputRequiredResult`; see the New entry below). Sampling is the exception: it is deprecated on every era and will not return on modern connections (see the Deprecated entry below).
|
||||
|
||||
Ordinary `ctx.info` usage emits an SDK-level `MCPDeprecationWarning` ("The logging capability is deprecated as of 2026-07-28 (SEP-2577)"). That warning comes from the SDK, not FastMCP, and is benign — logging keeps working on session-based connections per the matrix. `ctx.sample`/`ctx.sample_step` additionally emit a FastMCP-owned `FastMCPDeprecationWarning` (see below). The upgrade guide calls both out explicitly.
|
||||
|
||||
Wire interop across the transition is verified: a 3.4.3 client against a v4 server and a v4 client against a 3.4.3 server are bidirectionally clean across 9 operations over HTTP (WS2).
|
||||
|
||||
*Verify:* `docs/getting-started/upgrading/from-fastmcp-3.mdx` (the published matrix and SDK-warning note), `tests/server/test_protocol_eras.py`.
|
||||
|
||||
### Sampling deprecated, era-gated — Deprecated
|
||||
|
||||
`ctx.sample()` and `ctx.sample_step()` are deprecated and slated for removal in a future FastMCP release. Server-initiated sampling relies on the `createMessage` back-channel that SEP-2577 removed from the wire as of `2026-07-28`, and unlike elicitation it has no multi-round-trip replacement (the agentic loop would exhaust the round-trip budget). Both methods now emit a `FastMCPDeprecationWarning` once per process (gated on `settings.deprecation_warnings`), and on a `2026-07-28` connection they raise a clear `ToolError` before touching the wire. The client-side sampling handler infrastructure (anthropic/openai/google_genai) is retained for future MRTR work and is not deprecated. The migration is to call an LLM directly from your server rather than borrowing the client's model.
|
||||
|
||||
The dead TODO at `server/context.py` (a background-task sampling relay that was never built) is removed: that relay is not being built, so the note is gone rather than left as a promise.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/context.py` (`_warn_sampling_deprecated`, `_is_modern_protocol`, the `sample`/`sample_step` gates), `docs/servers/sampling.mdx` (deprecation banner), `tests/server/test_protocol_eras.py` (warning + era-gate tests).
|
||||
|
||||
### Push-feature degradation quality — Resolved (was sdk-feedback #10)
|
||||
|
||||
On a `2026-07-28` connection the degradation error used to differ by feature: `ctx.list_roots` raised a clear `NoBackChannelError`, while `ctx.elicit` / `ctx.sample` surfaced a bare "Method not found" because those methods attach a `related_request_id` and reach client dispatch before failing. FastMCP now era-gates `ctx.elicit` and `ctx.sample`/`ctx.sample_step` to raise a clear, era-aware `ToolError` before the wire ("server-initiated sampling is not available on MCP 2026-07-28 connections…" and "elicitation via server-initiated requests is unavailable on 2026-07-28 connections."). The strict xfail that captured #10 is flipped to a passing test.
|
||||
|
||||
*Verify:* `tests/server/test_protocol_eras.py` (`test_elicit_sample_degradation_message_is_clear_on_modern`, now a real test), `server/context.py` (era gates).
|
||||
|
||||
### Server-level cache hints (SEP-2549) — New (opt-in feature)
|
||||
|
||||
A FastMCP server can emit SEP-2549 freshness hints so a caching client (`fastmcp.Client(cache=...)`) may reuse a response without a wire round-trip. Two constructor params carry it: `FastMCP(cache_ttl=300, cache_scope="public")`, where `cache_ttl` is in seconds and `cache_scope` is `"public"` or `"private"` (default `"private"` when a TTL is set). The hint is uniform by construction — one server-level value applies to every SDK-cacheable method (`tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, `resources/read`, and `server/discover`) with no per-component surface and no aggregation. FastMCP does not hand-set the wire fields: it passes the hint through to the SDK low-level `Server(cache_hints=...)`, whose runner fills `ttlMs`/`cacheScope` on every cacheable result via `apply_cache_hint`, leaving any field a handler set explicitly untouched. `cache_ttl` must be positive, and a `cache_scope` without a `cache_ttl` is rejected at construction (a scope alone does not enable caching, since the client gates on the TTL's presence). Absent both params, no hint is emitted. Honoring is modern-only (the SDK client reads hints only at `2026-07-28`) and opt-in on the client, so a hinted server is inert unless the client passes `cache=`.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/caching.py` (`build_cache_hints`), `fastmcp_slim/fastmcp/server/server.py` (constructor params passed to `LowLevelServer(cache_hints=...)`), `tests/server/test_cache_hints.py` (unit validation + end-to-end interop with `fastmcp.Client(cache=True)`).
|
||||
|
||||
### Elicitation on the modern protocol (SEP-2322), guard form — New (opt-in feature)
|
||||
|
||||
A tool can gather client input across rounds on a `2026-07-28` call by returning an `InputRequiredResult` (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)). Each round is a complete request→response cycle: the tool re-runs per round and reads the client's answers off two new `Context` properties, `ctx.input_responses` (`None` on the first round) and `ctx.request_state` (the echoed opaque state) — thin passthroughs matching the SDK's mcpserver semantics. This is the modern-era elicitation path the earlier per-feature matrix flagged as "MRTR rewrite pending"; it mirrors the SDK's base guard model exactly (tool re-runs, checks whether answers are present, returns to ask for more), with no FastMCP-invented resolver or annotation layer. For authoring these requests, `InputRequiredResult`, `ElicitRequest`, and `ElicitRequestFormParams` import from `mcp_types`. The `request_state` channel is sealed by the framework, not the author: FastMCP installs the SDK's `RequestStateBoundary` middleware on its low-level server, which seals every outgoing `request_state` and unseals and verifies every inbound echo before a tool runs — so a tool only ever sees plaintext and a tampered, expired, or foreign token is rejected with a frozen wire error. `FastMCP(request_state_security=RequestStateSecurity(keys=[...]))` supplies shared keys for multi-replica deployments; omitted, each process seals under an ephemeral key (correct single-process). Returning this result on a handshake-era (≤ 2025-11-25) connection raises a clear era error naming the mismatch rather than failing as a generic invalid result. The client half (`fastmcp.Client` at `mode="auto"`) drives the loop through its existing elicitation/sampling/roots handlers, capped by `input_required_max_rounds`.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/context.py` (`input_responses`/`request_state` properties), `fastmcp_slim/fastmcp/server/low_level.py` (`RequestStateBoundary` install), `fastmcp_slim/fastmcp/server/server.py` (`request_state_security` param), `fastmcp_slim/fastmcp/server/mixins/mcp_operations.py` (`_on_call_tool` input-required passthrough + era gate), `fastmcp_slim/fastmcp/tools/base.py` (`InputRequiredToolResult`), `tests/server/test_mrtr_guards.py`.
|
||||
|
||||
### Proxy era mirroring — New (behavior)
|
||||
|
||||
A proxy is a server on its front and a client on its back, and the two eras have mutually exclusive interaction models on a single session: the handshake era pushes server-initiated requests (sampling/elicitation/roots) that the proxy forwards to its client, while the modern era forbids those and round-trips a guard tool's `InputRequiredResult` as a result instead. A proxy created from a non-Client target with no explicit `mode` now MIRRORS the front connection's negotiated era onto its backend session per request, so the whole chain speaks one era end-to-end — a modern client reaches a modern backend (guard round-trips work), a handshake client reaches a handshake backend (push-forwarding works), and the same proxy serves both without a backend session ever crossing eras. Because the default factory builds a fresh backend client per request and derives its `mode` from the front era at call time, only the metadata-only component caches are shared across eras. An explicit `create_proxy(target, mode=...)` still pins the backend era regardless of the front, overriding mirroring for a backend that only speaks one era; the resulting cross-era feature mismatches surface through the existing era gates. `ProxyInitializeMiddleware` no longer force-calls the handshake-only `client.initialize()` when the backend negotiated the modern era, so an explicit modern pin behind a handshake front no longer crashes on connect. The mirrored era carries through a multi-server `MCPConfig` target as well: that form mounts one proxy per configured server onto a composite router, and `TransportOptions.backend_mode` hands the era down to those mounted legs so every real backend negotiates it, not just the router in front of them. That router is also now sealed under a policy held on the transport rather than a fresh per-router ephemeral key, so a guard tool's `request_state` survives the router being rebuilt between rounds.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/providers/proxy.py` (`_mirror_front_era_mode`, the `_create_client_factory` non-Client branch, the era guard in `ProxyInitializeMiddleware.on_initialize`), `fastmcp_slim/fastmcp/client/transports/base.py` (`TransportOptions.backend_mode`), `fastmcp_slim/fastmcp/client/transports/config.py` (`MCPConfigTransport.connect_session` / `_create_proxy`), `fastmcp_slim/fastmcp/server/server.py` (`create_proxy` docstring), `tests/server/test_mrtr_guards.py` (`TestProxyEraMirroring`, `TestMultiServerConfigEraMirroring`).
|
||||
|
||||
### Resource and prompt errors survive the modern era — Absorbed (defect fix)
|
||||
|
||||
`_on_call_tool` returns a `ResourceError`-equivalent as an error result, but `_on_read_resource` and `_on_get_prompt` caught only `DisabledError`/`NotFoundError`, so a `ResourceError`, `PromptError`, or an argument-conversion failure on a resource template escaped as a raw handler exception. On the handshake eras that reached the wire as `str(exc)`, which is survivable; on `2026-07-28` the runner masks anything that is not an `MCPError` or `ValidationError` as a generic `"Internal server error"`, so a legitimate client-input error became indistinguishable from a server bug. Both handlers now translate a `FastMCPError` through `to_mcp_error` the way tools already do. Masking is unchanged — `mask_error_details` is still applied inside `read_resource`/`render_prompt`, so these paths leak no more than tools do.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/mixins/mcp_operations.py` (`_on_read_resource`, `_on_get_prompt`), `tests/server/test_protocol_eras.py`.
|
||||
|
||||
### Proxies forward upstream instructions on the modern era — Absorbed (defect fix)
|
||||
|
||||
`ProxyInitializeMiddleware` forwards an upstream server's `instructions` by patching the `InitializeResult`, but `on_initialize` only fires for the handshake era. A modern client negotiates via `server/discover`, which the SDK builds from the low-level server's own `instructions`, so a proxy silently dropped its upstream's instructions for every modern client. `FastMCPProxy` now registers a `server/discover` handler (the same `add_request_handler` hook it already uses for `ping`, and a replacement the SDK explicitly sanctions) that delegates to the SDK's own implementation and fills in only the instructions that would otherwise be lost. The proxy's lazy-connect contract is unchanged: the backend is contacted when a client asks, never at construction. Because era mirroring pins a modern backend to an exact version — and a pinned version adopts a synthesized `DiscoverResult` rather than probing the wire — this read negotiates with `mode="auto"`; instructions are metadata with no back-channel, so they do not need the era consistency mirroring exists to protect.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/providers/proxy.py` (`FastMCPProxy._setup_proxy_discover_handler`), `tests/server/providers/proxy/test_proxy_server.py` (`TestProxyModernEraInstructions`).
|
||||
|
||||
### Proxy list methods raise `MCPError` on backend failure — Breaking (in-process error type)
|
||||
|
||||
`ProxyProvider`'s four `_list_*` methods caught only `MCPError`, so a failed backend connection escaped as the `RuntimeError` the client wraps it in (or a raw `httpx2.ConnectError`). On the handshake eras that reached the wire as `str(exc)` and named the real failure; on `2026-07-28` it was masked as `"Internal server error"`, leaving a modern client unable to tell a dead backend from a server bug. The list methods now normalize transport failures through `_proxy_upstream_error`, matching `ProxyInitializeMiddleware.on_initialize`. Code calling a proxy's `list_tools()` (and friends) in-process must now catch `MCPError` rather than `RuntimeError`; the over-the-wire error type is unchanged.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/providers/proxy.py` (`_PROXY_TRANSPORT_ERRORS` and the four `_list_*` methods), `tests/server/providers/proxy/test_proxy_server.py` (`TestProxyProviderTransportErrors`).
|
||||
|
||||
### The xfail register — Known gap
|
||||
|
||||
Roughly forty `xfail` markers across the test tree (concentrated in `tests/server/tasks/`, `tests/client/tasks/`, and `test_protocol_eras.py`) are the built-in beta tracker: each names the SDK gap it waits on. They are enumerated and mapped to sdk-feedback findings on the [Known Gaps](/development/v4-notes/known-gaps) page.
|
||||
|
||||
## Security
|
||||
|
||||
FastMCP retains hardening that is not yet upstream and does not remove it during the migration.
|
||||
|
||||
### Retained OAuth / DCR hardening — Absorbed
|
||||
|
||||
FastMCP keeps its own DCR redirect-URI hardening (PRs #4419, #4408) regardless of the SDK's validation, which still accepts unsafe `javascript:`/`data:` redirect schemes at the model level (sdk-feedback #4). The streamable-HTTP DNS-rebinding protection above is a second retained security surface.
|
||||
|
||||
*Verify:* recent commits `67527c1f` (block unsafe OAuth redirect schemes), `57a27992` (DNS rebinding), `cccb529f` (DCR redirect URI validation) on `main`.
|
||||
|
||||
### Identity assertion (SEP-990 ID-JAG) — Added (beta)
|
||||
|
||||
`OAuthProxy` (and `OIDCProxy`, which inherits it) accepts an optional `identity_assertion=IdentityAssertion(trusted_issuers=[...])`. When configured, the token endpoint accepts the RFC 7523 `urn:ietf:params:oauth:grant-type:jwt-bearer` grant carrying an enterprise IdP-issued ID-JAG, validates it (signature against the trusted issuer's JWKS, `iss`/`aud`/`exp`, `typ` of `oauth-id-jag+jwt`, mandatory `sub`, signed `client_id`/`resource` binding, and `jti` replay rejection), and mints a short-lived FastMCP access token carrying the asserted subject with no refresh token. Authorization server metadata advertises the `jwt-bearer` grant type and the `urn:ietf:params:oauth:grant-profile:id-jag` profile when enabled. This is server-side only; the client-side wrapper ships separately. See [Identity Assertion](/servers/auth/oauth-proxy#identity-assertion-sep-990).
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/auth/identity_assertion.py`, the `exchange_identity_assertion` and `get_routes` changes in `fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py`, and the jwt-bearer dispatch in `fastmcp_slim/fastmcp/server/auth/auth.py` (`TokenHandler._maybe_handle_id_jag`).
|
||||
|
||||
### Templated resource parameters are path-screened by default — Breaking (behavior)
|
||||
|
||||
Every templated resource now has its extracted parameter values screened for path-traversal (`..` segments), absolute paths, and null bytes **before the handler runs** — on by default, at the server's read chokepoint, covering local and provider-sourced (mounted/proxied) templates alike. Previously these payloads reached handlers raw; a template whose parameter flowed into a filesystem path or upstream URL was exposed unless the author added their own check. A rejected read now surfaces a non-leaky "resource not found" error (`-32602`) and a debug log.
|
||||
|
||||
The check is component-based, matching the SDK's `contains_path_traversal`: only a standalone `..` segment is traversal, so values that merely contain dots (`HEAD~3..HEAD`, `file.tar.gz`) and dotfiles (`.env`) still pass. This can break a template that legitimately accepts `..`-bearing or absolute values — exempt the parameter with `ResourceSecurity(exempt_params={...})`, disable per-component with `security=None`, or set a server-wide default with `FastMCP(resource_security=...)`. See [Resources → Path Security](/servers/resources#path-security).
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/resources/security.py` (`ResourceSecurity`), the screening block in `FastMCP.read_resource` (`fastmcp_slim/fastmcp/server/server.py`), and `tests/resources/test_resource_security.py`.
|
||||
|
||||
## Removed in 4.0
|
||||
|
||||
Deprecations that warned in 3.x are removed in 4.0. Each entry below is a hard removal — the old surface raises `TypeError` / `AttributeError` rather than warning, unless noted otherwise.
|
||||
|
||||
### Module and class shims
|
||||
|
||||
- **`fastmcp.server.proxy`** (deprecated 3.0) — Breaking. Import proxy classes (`FastMCPProxy`, `ProxyClient`, etc.) from `fastmcp.server.providers.proxy` instead.
|
||||
- **`fastmcp.server.openapi`** and its submodules (`server`, `components`, `routing`), including the **`FastMCPOpenAPI`** class (deprecated 3.0) — Breaking. Use `FastMCP` with an `OpenAPIProvider` from `fastmcp.server.providers.openapi` instead.
|
||||
- **`fastmcp.experimental.server.openapi`** and **`fastmcp.experimental.utilities.openapi`** shims (deprecated 2.14) — Breaking. Import from `fastmcp.server.providers.openapi` and `fastmcp.utilities.openapi` respectively.
|
||||
- **`fastmcp.server.apps`** and **`fastmcp.server.app`** shims (deprecated 3.2) — Breaking. Import from `fastmcp.apps` (e.g. `AppConfig`) or `fastmcp` (`FastMCPApp`) instead.
|
||||
- **`PromptToolMiddleware`** and **`ResourceToolMiddleware`** (deprecated 3.1) — Breaking. Use the `PromptsAsTools` / `ResourcesAsTools` transforms from `fastmcp.server.transforms` instead. The non-deprecated `ToolInjectionMiddleware` base class is retained.
|
||||
- **`StreamableHttpTransport(sse_read_timeout=...)`** (deprecated no-op) — Breaking. The parameter had no effect under the SDK v2 client; configure timeouts via `read_timeout_seconds` in `session_kwargs` or on the httpx2 client via `httpx_client_factory`. `SSETransport` still accepts `sse_read_timeout`.
|
||||
|
||||
### `FastMCP` server methods and `mount()` kwargs
|
||||
|
||||
The following `FastMCP` methods and parameters, deprecated since 3.0, are removed:
|
||||
|
||||
- `FastMCP.as_proxy(...)` → `create_proxy(...)` (`from fastmcp.server import create_proxy`)
|
||||
- `FastMCP.import_server(sub)` → `mount(sub)`
|
||||
- `mount(prefix=...)` → `mount(namespace=...)`
|
||||
- `mount(as_proxy=...)` — removed; mounts always invoke the child's lifespan and middleware, so the flag was already meaningless. To proxy a server, wrap it with `create_proxy()` before mounting.
|
||||
- `FastMCP.add_tool_transformation(name, config)` → `add_transform(ToolTransform({name: config}))`
|
||||
- `FastMCP.remove_tool_transformation(name)` — removed; it was a no-op that only warned (transforms are immutable once added). Use `server.disable(keys=[...])` to hide tools.
|
||||
- `FastMCP.remove_tool(name)` → `mcp.local_provider.remove_tool(name)`
|
||||
|
||||
The `_REMOVED_KWARGS` constructor shim (which raises helpful `TypeError`s for kwargs removed in 3.0) is retained through 4.0.
|
||||
|
||||
### Tool and component parameters
|
||||
|
||||
- **Tool-level `serializer` parameter** — removed from `@tool` / `mcp.tool()`, `Tool.from_function`, `Tool.from_tool`, `TransformedTool.from_tool`, the OpenAPI `OpenAPITool`, and the `mcp_mixin` tool decorator. Return a `ToolResult` from your tool for full control over serialization instead (see [Custom Serialization](/servers/tools#custom-serialization)). The server-level `tool_serializer` constructor kwarg was already removed in 3.0.
|
||||
- **Tool `exclude_args` parameter** — removed from the tool decorator and its plumbing (`ParsedFunction.from_function`, `Tool.from_function`, `mcp.tool()`). Use dependency injection with `Depends()` to hide parameters from the tool schema instead.
|
||||
- **`decorator_mode` setting** (`FASTMCP_DECORATOR_MODE`) and its `"object"` mode — removed. Decorators always return the original function with metadata attached; the object-returning machinery is gone. Access component objects through the server (e.g. `await mcp.get_tool("name")`) rather than the decorated function.
|
||||
- **Component-import compatibility shims** — the `__getattr__` shims that re-exported `FunctionTool` / `ParsedFunction` / `tool` from `fastmcp.tools.tool`, `FunctionResource` / `resource` from `fastmcp.resources.resource`, and `FunctionPrompt` / `prompt` from `fastmcp.prompts.prompt` are removed. Import these from their canonical modules (`fastmcp.tools.function_tool`, `fastmcp.resources.function_resource`, `fastmcp.prompts.function_prompt`) instead.
|
||||
|
||||
*Verify:* deletions of `fastmcp_slim/fastmcp/server/proxy.py`, `fastmcp_slim/fastmcp/server/openapi/`, `fastmcp_slim/fastmcp/experimental/server/openapi/`, `fastmcp_slim/fastmcp/experimental/utilities/openapi/`, `fastmcp_slim/fastmcp/server/apps.py`, `fastmcp_slim/fastmcp/server/app.py`; the removed classes in `fastmcp_slim/fastmcp/server/middleware/tool_injection.py`; the removed parameter in `fastmcp_slim/fastmcp/client/transports/http.py`; `fastmcp_slim/fastmcp/server/server.py`; `fastmcp_slim/fastmcp/tools/base.py`, `tools/function_tool.py`, `tools/tool_transform.py`, `tools/function_parsing.py`; `fastmcp_slim/fastmcp/settings.py`, `resources/function_resource.py`, `prompts/function_prompt.py`, and the local-provider decorators; `resources/base.py`, `prompts/base.py`.
|
||||
146
docs/development/v4-notes/feature-program.mdx
Normal file
146
docs/development/v4-notes/feature-program.mdx
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
---
|
||||
title: Feature Program
|
||||
---
|
||||
|
||||
The migration is the foundation. The forward v4 program is a sequence of post-merge PRs that build on it. Several have now merged. Each feature below carries an explicit status:
|
||||
|
||||
- **Shipped** — merged to `main`, with the PR cited.
|
||||
- **Designed** — the approach is settled and an API sketch exists; implementation has not started.
|
||||
- **Planned** — the shape is agreed but design details remain open.
|
||||
- **Not started** — identified as v4 scope, not yet designed.
|
||||
|
||||
Code blocks marked as sketches show the *intended* API and do not resolve against the current tree.
|
||||
|
||||
## Sampling removal
|
||||
|
||||
**Status: Deprecation and era-gating shipped (#4448); removal slated for 4.0.**
|
||||
|
||||
Sampling is the push-shaped API where a server borrows the client's model mid-call (`ctx.sample`, `ctx.sample_step`). The `2026-07-28` era removes server-initiated requests, so this API cannot work on modern connections. Background-task sampling is dead under v2 — a worker's back-channel is gone once the submitting request returns, and no sampling relay was ever built (sdk-feedback #9).
|
||||
|
||||
The plan is Option A: **deprecate the push-sampling API now and remove it in the 4.0 release.** The first two steps shipped in #4448:
|
||||
|
||||
- **Done:** `ctx.sample` / `ctx.sample_step` emit a `FastMCPDeprecationWarning` (once per process, gated on `settings.deprecation_warnings`).
|
||||
- **Done:** both are era-gated to raise a clear, era-aware `ToolError` on `2026-07-28` before the wire, which also fixed the opaque "Method not found" of sdk-feedback #10.
|
||||
- **Pending 4.0:** remove `ctx.sample`, `ctx.sample_step`, `server/sampling/`, `SamplingTool`, and structured-result sampling.
|
||||
|
||||
The migration story is honest: there is **no drop-in** on modern connections. The guidance is architectural — call an LLM from your server directly, with your own API key, rather than borrowing the client's model. That shift is the real answer, and it is why the removal justifies a major version.
|
||||
|
||||
The client-side provider handlers (Anthropic, OpenAI, Google GenAI) are **retained** regardless: MRTR needs them to answer sampling input-requests from the client side. What is removed is the server-side push emitter, which the SDK never built for the modern era.
|
||||
|
||||
Sampling still functions on the legacy eras. Users also see an SDK-level `MCPDeprecationWarning` on ordinary `ctx.sample` usage (the SDK deprecated the capability wire-side per SEP-2577). FastMCP's own deprecation — the warning with migration guidance, plus the era-gating — shipped in #4448; only the final removal remains for 4.0.
|
||||
|
||||
## MRTR elicitation
|
||||
|
||||
**Status: Guard form shipped (4.0). Declarative `Resolve` layer designed.**
|
||||
|
||||
Elicitation survives the modern era through multi-round-trip (MRTR). The 2026 wire envelope carries elicitation as a multi-round input-request: a tool returns an `InputRequiredResult` and re-runs per round, each round a complete request→response cycle. Imperative `ctx.elicit` relies on the session back-channel, which is gone on `2026-07-28` foreground calls; on the modern era, elicitation is reachable through MRTR instead.
|
||||
|
||||
The **guard form** of this is shipped in 4.0 (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)): a tool returns an `InputRequiredResult` and reads the client's answers off `ctx.input_responses` / `ctx.request_state`, re-running each round. It mirrors the SDK's base guard model exactly — no FastMCP-invented DX, the framework owns `request_state` sealing, and returning this result on a handshake-era connection produces a clear era error.
|
||||
|
||||
What remains is the declarative `Resolve(...)` layer that sits *on top of* that shipped primitive. It is designed, not built: a new `fastmcp.elicitation` module — `Resolve`, `Elicit`, and `ElicitationResult` — thin wrappers over the SDK's resolver, wired into FastMCP's own tool layer (FastMCP tools do not inherit the SDK's auto-resolver wiring). It would detect `Annotated[_, Resolve(...)]` parameters, build resolver plans, and return the SDK's `InputRequiredResult` instead of the tool body on the first round.
|
||||
|
||||
Imperative `ctx.elicit` is **not** re-plumbed to survive the modern era. It works on the legacy eras through the session back-channel, and on `2026-07-28` foreground calls it is era-gated to raise a clear error (shipped in #4448) pointing at the guard form. The earlier plan to keep imperative `ctx.elicit` alive on modern connections through a background-task relay is dead twice over: the guard model shipped in its place, and the 2025 task machinery the relay depended on is slated for removal (see [Known Gaps](/development/v4-notes/known-gaps#the-xfail-register)).
|
||||
|
||||
The intended declarative DX (sketch — the module does not exist yet):
|
||||
|
||||
```python test="skip"
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from fastmcp import FastMCP, Context
|
||||
from fastmcp.elicitation import Resolve, Elicit, ElicitationResult
|
||||
|
||||
mcp = FastMCP("shipping")
|
||||
|
||||
|
||||
class Address(BaseModel):
|
||||
street: str
|
||||
city: str
|
||||
zip: str
|
||||
|
||||
|
||||
async def ask_address(ctx: Context) -> Elicit[Address]:
|
||||
return Elicit("Where should we ship this order?", Address)
|
||||
|
||||
|
||||
@mcp.tool
|
||||
async def create_shipment(
|
||||
order_id: str,
|
||||
address: Annotated[Address, Resolve(ask_address)], # unwrapped; decline -> ToolError
|
||||
) -> str:
|
||||
return f"Shipping {order_id} to {address.city}"
|
||||
|
||||
|
||||
@mcp.tool
|
||||
async def maybe_ship(
|
||||
order_id: str,
|
||||
address: Annotated[ElicitationResult[Address], Resolve(ask_address)], # full outcome
|
||||
) -> str:
|
||||
if address.action != "accept":
|
||||
return "cancelled"
|
||||
return f"Shipping {order_id} to {address.data.city}"
|
||||
```
|
||||
|
||||
The FastMCP client already dispatches input-requests through its elicitation callback; the remaining declarative work confirms the FastMCP client drives the input-required driver the way the SDK's own client does.
|
||||
|
||||
The divergence between elicitation and sampling on 2026 comes down to one fact: the SDK built the server-side emitter for elicitation (`Elicit`/`Resolve`) and not for sampling. The wire carries all three input-request types and the client dispatches all three; only elicitation can produce one server-side. That is why elicitation survives 4.0 via MRTR and push-sampling does not.
|
||||
|
||||
## Middleware root dispatch
|
||||
|
||||
**Status: Shipped (#4553).**
|
||||
|
||||
The migration already routed `initialize` interception through the SDK's `ServerMiddleware` list via `FastMCPServerMiddleware`. #4553 made that entry the root of middleware dispatch: FastMCP's method-agnostic hooks (`on_message`, `on_request`, `on_notification`) now fire for every inbound message — client cancellations, progress notifications, and requests that fail routing or validation — not only the ones that reach a component handler. The component methods keep running their own chain interior, and a method set plus a dispatch flag keep the two passes disjoint so each hook fires exactly once per message.
|
||||
|
||||
## First-class 2026 client
|
||||
|
||||
**Status: Partly shipped (#4572, #4574); full composition blocked upstream.**
|
||||
|
||||
`fastmcp.Client` now defaults to `mode="auto"` (#4572): it probes `server/discover`, falls back to the classic handshake, and answers multi-round-trip `input_required` requests through its existing handlers. The same PR surfaced `extensions=` and `result_claims=` (SEP-2133). The client also dropped its forked protocol helpers — extension folding, the evicting message handler, discover synthesis — in favor of the SDK's own (#4574).
|
||||
|
||||
The decision here was **compose, not wrap** (D16): rebuild `fastmcp.Client` on the SDK's high-level `mcp.Client` rather than wrapping `mcp.ClientSession`. The parts that compose cleanly have shipped. The rest is **blocked upstream on two counts**. First, `mcp.Client` constructs its `ClientSession` at a single hardcoded site with no injection hook, while FastMCP's `session_class` is load-bearing (`ProxyClient` substitutes a session that skips result validation so a backend's schema violation surfaces at the end client rather than becoming a proxy error) — a `session_factory=` hook on `mcp.Client`, the same shape as the `notification_bindings=` parameter added earlier, would solve this. Second, `mcp.Client.__aenter__` refuses reentry, but FastMCP's client is deliberately reentrant (its refcounted context manager exists to fix a proxy session-reuse deadlock), so the rebuild also needs the SDK client to tolerate reentrant entry. Both must land upstream before the full rebuild is possible; `session_factory=` alone is necessary but not sufficient.
|
||||
|
||||
This workstream also owns the server-side statelessness design holes — `ctx.session_id` / `set_state` round-tripping and stateful-proxy affinity — since they turn on the same "what is a session without a session?" question. See [Statelessness on 2026-07-28](/development/v4-notes/known-gaps#statelessness-on-2026-07-28) for the full accounting.
|
||||
|
||||
## Subscriptions, cache hints, extensions, OTel
|
||||
|
||||
**Status: Mixed — cache hints and OTel shipped; subscriptions not started.**
|
||||
|
||||
A cluster of protocol features tracked for v4. Their statuses have diverged:
|
||||
|
||||
- **Cache hints — shipped (#4464).** Server-level authoring (`FastMCP(cache_ttl=..., cache_scope=...)`, SEP-2549) stamps every cacheable result, and the FastMCP client honors hints with an opt-in response cache.
|
||||
- **OpenTelemetry — shipped (#4481).** Spans are on by default (a no-op without an exporter), with SDK-aligned attributes and a `FASTMCP_ENABLE_TELEMETRY=false` off-switch.
|
||||
- **Extensions — client side shipped (#4572).** `Client(extensions=..., result_claims=...)` advertises opt-in client extensions (SEP-2133). The server side is a Designed workstream in its own right (see [FastMCP-native extension API](#fastmcp-native-extension-api)). The cross-era reconciliation of the `extensions` / MCP Apps capability advertisement is still open (the capability is stripped at pre-2026 negotiated versions — sdk-feedback #2).
|
||||
- **Subscriptions — not started.** A `subscriptions/listen` surface backed by a subscription bus.
|
||||
|
||||
## FastMCP-native extension API
|
||||
|
||||
**Status: Shipped (#4602).**
|
||||
|
||||
MCP extensions (SEP-2133) are optional, capability-negotiated protocol features identified by a reverse-DNS string — `io.modelcontextprotocol/ui` (MCP Apps), `io.modelcontextprotocol/tasks` (SEP-2663). They are a genuinely new abstraction in SDK v2; they did not exist in v1. The SDK exposes them through an `Extension` server class that contributes a capability, additive request methods, and a `tools/call` interceptor, plus a symmetric `ClientExtension` with result claims and notification bindings.
|
||||
|
||||
FastMCP already forwards `ClientExtension` natively (`Client(extensions=...)`, #4572). The **server** side does not use the SDK's `Extension` class at all: MCP Apps predates the abstraction, so FastMCP hand-splices the `ui` capability into `get_capabilities()` on the low-level server and walks tool metadata directly. That worked for one extension, but every new protocol extension currently means bespoke surgery on core.
|
||||
|
||||
The Designed work is a FastMCP-native server extension API — a single registration point (`mcp.add_extension(...)`) that contributes a negotiated capability, request methods, and a `tools/call` interceptor, with access to FastMCP-level constructs the SDK's `Extension` withholds (the component registry, `Context`, auth scope). It is designed against the SEP-2663 tasks extension because tasks exercises the full surface — capability *and* methods *and* interception *and* client claims/notifications — where MCP Apps exercises only a subset. Tasks is the pathfinder; MCP Apps migrates onto the extension API as a fast-follow, deleting the hand-rolled splices, and confirms the design generalizes. The discriminator that keeps the extension API distinct from [middleware](/servers/middleware): an extension is a *negotiated contract change* the client must understand, where middleware is unilateral server behavior the client never sees. Delete a capability advertisement and nothing about the client changes — that is middleware, not an extension.
|
||||
|
||||
## Background tasks (SEP-2663)
|
||||
|
||||
**Status: Shipped (#4603).**
|
||||
|
||||
Background tasks return to the modern era as `fastmcp-tasks`, an in-repo optional package rebuilt on the `io.modelcontextprotocol/tasks` extension (SEP-2663, Final, merged upstream 2026-05-15). SEP-2663 supersedes SEP-1686 but keeps its polling core: a client that advertises the tasks capability issues an augmented `tools/call`; the server decides whether to run it as a task and returns a `CreateTaskResult` carrying a server-generated task id; the client polls `tasks/get` until terminal and reads the result inlined there. FastMCP's existing SEP-1686 wire layer is removed while the Docket/Redis execution engine underneath moves into `fastmcp-tasks` intact — the spec moved toward what FastMCP already built, so the rebuild is mostly deletion plus a thin wire adapter. `task=True` stays the authoring surface (gated by the `fastmcp[tasks]` extra and an explicit `mcp.add_extension(TasksExtension(...))`, the first consumer of the [extension API](#fastmcp-native-extension-api) above), so a server that already uses tasks needs no code change. Scope for v1 is polling-only and `tools/call`-only.
|
||||
|
||||
The full design — wire delta, the engine/wire split, packaging, client experience, sequencing, risks, and the five resolved decisions — is on the dedicated [Background Tasks (SEP-2663)](/development/v4-notes/background-tasks) page.
|
||||
|
||||
## SDK delegation, round two
|
||||
|
||||
**Status: Planned (gated on upstream).**
|
||||
|
||||
The real HTTP simplification is a v4 project, not this PR. FastMCP can collapse its `create_streamable_http_app` onto the SDK's `Server.streamable_http_app()` once upstream adds three things:
|
||||
|
||||
1. per-session event-store scoping,
|
||||
2. a user-middleware injection hook,
|
||||
3. a lifespan hook.
|
||||
|
||||
The payoff is not only less code — FastMCP would also inherit the SDK's session-owner credential enforcement, a security gain it lacks today. These are the three upstream feature requests to file (alongside the advisory dossier described in [Known Gaps](/development/v4-notes/known-gaps)). Until they land, the four HTTP overrides in the [Change Register](/development/v4-notes/change-register#http) stay.
|
||||
|
||||
One latent capability worth surfacing on FastMCP's side: `session_idle_timeout` is accepted by the manager but never set by `create_streamable_http_app` — a one-line plumb if FastMCP wants to expose it.
|
||||
49
docs/development/v4-notes/index.mdx
Normal file
49
docs/development/v4-notes/index.mdx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
title: v4.0 Development Notes
|
||||
---
|
||||
|
||||
This directory is the working map of FastMCP v4.0: the complete register of user-facing changes from the MCP Python SDK v2 migration ([PR #4437](https://github.com/PrefectHQ/fastmcp/pull/4437)), plus the forward v4 feature program. It plays three roles at once.
|
||||
|
||||
1. **A change register.** Every user-visible change from the migration, organized by subsystem, with a note on how FastMCP handles it (absorbed, bridged, breaking, or deprecated) and where to find it in the diff. This is the [Change Register](/development/v4-notes/change-register).
|
||||
2. **A feature program.** The forward v4 work — sampling removal, multi-round-trip elicitation, the first-class 2026 client, a FastMCP-native extension API, the SEP-2663 background-tasks rebuild, and the SDK-delegation round-two convergence — now a mix of shipped, designed, and pending. Multi-round-trip guard tools (#4544), the client's `mode="auto"` default with a partial SDK-composition (#4572/#4574, full composition blocked upstream), the extension API (#4602), and background tasks on SEP-2663 (#4603) have shipped; sampling removal and SDK delegation remain ahead. Each carries an explicit status in the [Feature Program](/development/v4-notes/feature-program). The shipped side — what a v4 deployment provides on the modern protocol today, including the complete server-side SEP-990 identity assertion implementation — is cataloged in [2026-07-28 Protocol Support](/development/v4-notes/protocol-2026).
|
||||
3. **A review lens.** Because the migration PR is too large to review line by line, the change register is organized so a reviewer can take one subsystem, read its claimed changes, and verify each against the diff. The [Known Gaps](/development/v4-notes/known-gaps) page collects the deliberate xfails and the upstream dependencies that gate the follow-up work.
|
||||
|
||||
## Why v4 exists
|
||||
|
||||
FastMCP v4.0 is an engine swap. Three forces drive the major version:
|
||||
|
||||
**The MCP Python SDK v2 rebuild.** The SDK v2 makes two sweeping changes to the protocol layer: it splits the protocol types out of `mcp.types` into a standalone `mcp_types` package, and it renames every protocol field from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, `isError` → `is_error`). It also rewrites the server request-handling model — handlers are now registered by method string and return bare result models, there is no `request_ctx` ContextVar, and server-side middleware is a first-class SDK concept. FastMCP absorbs almost all of this so that a typical server needs zero code changes.
|
||||
|
||||
**Protocol version 2026-07-28.** The SDK v2 serves multiple protocol eras from one server. Alongside the session-based handshake eras, it introduces the sessionless `2026-07-28` era, which discovers capabilities through `server/discover` and removes server-initiated requests (SEP-2577). This formally supersedes FastMCP's earlier "latest protocol only" stance: a single server now works with clients across the protocol transition.
|
||||
|
||||
**Sampling removal.** The `2026-07-28` era removes the server's ability to push a request back to the client mid-call. That takes the push-shaped sampling API (`ctx.sample`, `ctx.sample_step`) off the table on modern connections. Rather than leave it half-working, v4 deprecates it now and removes it in the 4.0 release — a real architectural shift for servers that borrowed the client's model, and one that justifies the major bump.
|
||||
|
||||
## Release strategy
|
||||
|
||||
The migration merges to `main` and development continues there with subsequent PRs. Releases follow the SDK's own beta timeline:
|
||||
|
||||
- **`main` carries the beta pins.** While the SDK is on `mcp==2.0.0b1` / `mcp-types==2.0.0b1`, `main` cuts **pre-releases** (`4.0.0b1`, `4.0.0b2`, …). No stable PyPI release goes out until `mcp 2.0.0` reaches GA — at which point the pins swap to the stable SDK and `4.0.0` ships. The pin-swap is a tracked checklist item on the [Known Gaps](/development/v4-notes/known-gaps) page.
|
||||
- **`release/3.x` is the maintenance line.** A `release/3.x` branch is cut from pre-merge `main`. It stays on the SDK v1 line, receives upstream security patches, and serves users who cannot move to the SDK v2 beta yet.
|
||||
|
||||
### Release codenames
|
||||
|
||||
Following the pun-title convention (`v<version>: <pun>`), the v4 line runs a single "four" motif across the whole cycle, holding the headline name for the stable release the way v3 did ("Three at Last" for `3.0.0`, stage puns for its betas):
|
||||
|
||||
| Release | Codename | The nod |
|
||||
| --- | --- | --- |
|
||||
| `4.0.0a1` (alpha) | **Fourst Contact** | _first contact_ — the first, cautious look at the new engine |
|
||||
| `4.0.0a2` (alpha) | **Back and Fourth** | _back and forth_ — the second pass, where background tasks and stateless state land |
|
||||
| `4.0.0b1` (beta) | **Fourgone Conclusion** | _foregone conclusion_ — once the MCP SDK went v2, v4 was inevitable |
|
||||
| `4.0.0b2` (beta) | **Fourmidable** | _formidable_ — held in reserve for a second beta if one is needed |
|
||||
| `4.0.0` (stable) | **Fast Fourward** | _fast forward_ — full speed onto the new foundation |
|
||||
|
||||
## How to read the register
|
||||
|
||||
Each subsystem section in the [Change Register](/development/v4-notes/change-register) tags its changes with one of four dispositions:
|
||||
|
||||
- **Absorbed** — the SDK changed underneath, but FastMCP's public surface is identical. Nothing for users to do.
|
||||
- **Bridged** — a compatibility shim keeps old code working, usually with a `FastMCPDeprecationWarning`. Users should migrate but are not forced to.
|
||||
- **Breaking** — user code must change. These are the headline migration items.
|
||||
- **Deprecated** — still works, warns now, slated for removal in a later release.
|
||||
|
||||
The user-facing summary of the migration lives in the published [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) guide. These development notes are the exhaustive version behind it.
|
||||
85
docs/development/v4-notes/known-gaps.mdx
Normal file
85
docs/development/v4-notes/known-gaps.mdx
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
title: Known Gaps and Upstream Dependencies
|
||||
---
|
||||
|
||||
The migration ships with a set of deliberate gaps: temporary shims, xfailed tests, and pins that depend on the MCP Python SDK v2 reaching GA. Each is tracked here with its removal trigger. This page is the checklist for the beta-to-stable transition and the advisory relationship with the SDK team.
|
||||
|
||||
## The xfail register
|
||||
|
||||
Roughly forty `xfail` markers across the test tree name the SDK gaps and removed protocol surfaces they wait on. Re-running the suite against a new SDK beta surfaces which have closed (a strict xfail that starts passing fails the suite, prompting removal of the marker). They cluster in three areas — but the largest cluster is no longer a set of gaps to close.
|
||||
|
||||
**Task suite (`tests/server/tasks/`, `tests/client/tasks/`) — SEP-1686 wire layer being removed; engine rebuilt on SEP-2663.** The large majority. These cover the 2025 task protocol (SEP-1686), which left the core MCP spec and was reworked into the `io.modelcontextprotocol/tasks` extension (SEP-2663). FastMCP's SEP-1686 *wire* machinery (capability advertisement, the `tasks/get|result|list|cancel` handlers, the push notification/elicitation relay) is slated for removal, so the wire-protocol xfails disappear with the code they cover — they are not waiting on an SDK fix. The Docket/Redis *execution engine* underneath is not discarded: it is extracted into the planned `fastmcp-tasks` package and re-adapted to the SEP-2663 polling shape (see [Background Tasks (SEP-2663)](/development/v4-notes/background-tasks)). The two SDK gaps these were originally filed against — **sdk-feedback #1** (SEP-1686 task result types omitted from the method registries) and **sdk-feedback #3** (no `task` field on `ReadResourceRequestParams` / `GetPromptRequestParams`) — are moot: they patched the SEP-1686 wire shape, which SEP-2663 replaces with a `CreateTaskResult` claimed on `tools/call`. The gap that matters for the rebuild is **sdk-feedback #2** (extensions capability stripped at pre-2026 negotiated versions) — it now gates a flagship feature and is escalated accordingly.
|
||||
|
||||
**Protocol eras (`tests/server/test_protocol_eras.py`).** One remaining strict xfail, and it too is task-related: the v2 SDK high-level client exposes no `task=` parameter on `call_tool`, so a SEP-1686 task-augmented `tools/call` cannot be submitted through it. It resolves with the SEP-1686 wire-layer removal above; the SEP-2663 rebuild submits tasks by advertising the extension capability and claiming a `CreateTaskResult`, not through a `task=` params field. The earlier strict xfail for the `ctx.elicit` / `ctx.sample` "Method not found" degradation (sdk-feedback #10) is **gone** — the era-gating shipped in #4448 flipped it to a passing test.
|
||||
|
||||
**MCP Apps (`tests/test_apps.py`).** Two xfails tied to **sdk-feedback #2** — the `extensions` capability is stripped by the pre-2026 version sieve, so the UI extension can't be advertised to legacy-era clients.
|
||||
|
||||
## Shims and their removal triggers
|
||||
|
||||
Every shim in the migration is temporary and carries a documented removal trigger.
|
||||
|
||||
| Shim | Location | Removal trigger |
|
||||
| --- | --- | --- |
|
||||
| `_sdk_patches.py` — task registry widening | `fastmcp_slim/fastmcp/_sdk_patches.py` | Removed with FastMCP's SEP-1686 wire machinery (`server/tasks/`), which is slated for removal now that the 2025 task protocol left the spec. The SEP-2663 rebuild does not need it — `CreateTaskResult` is claimed on `tools/call` through the extensions mechanism, which the SDK registries already admit. |
|
||||
| `_compat.py` — camelCase field bridge | `fastmcp_slim/fastmcp/_compat.py` | User-migration aid; removed in a future release after users migrate reads to snake_case. Users can preview removal with `mcp_camelcase_compat = False`. |
|
||||
| `FastMCPRequestContext` ContextVar | `fastmcp_slim/fastmcp/server/dependencies.py` | The SDK deliberately passes context as an argument with no ContextVar; FastMCP's public `get_context()` needs ambient access, and the shim also lifts `_meta`, which the SDK's `TypedDict` drops. No planned removal — this is a permanent boundary, not a beta gap. |
|
||||
| `FastMCPServerMiddleware` | `fastmcp_slim/fastmcp/server/low_level.py` | Already the native SDK `ServerMiddleware` path; no cleaner hook exists. Permanent. |
|
||||
| Client `get_session_id` header sniff | `fastmcp_slim/fastmcp/client/transports/http.py` | SDK exposes session id (or an `on_session_created` callback) from `streamable_http_client`, at parity with `sse_client` (sdk-feedback #5). |
|
||||
| `_sdk_context_shim.py` — generic handler aliases | `fastmcp_slim/fastmcp/client/_sdk_context_shim.py` | The SDK's `ClientRequestContext` is not subscriptable, so FastMCP keeps the public generic `SamplingHandler`/`RootsHandler`/`ElicitationHandler` aliases. Permanent unless the SDK makes the context subscriptable (sdk-feedback #7). |
|
||||
|
||||
The `TaskNotificationHandler` binding (sdk-feedback #8) is the client-side equivalent: it registers a `NotificationBinding` for the SEP-1686 `notifications/tasks/status` because the SDK no longer tees custom server notifications to the message handler. It goes away with the SEP-1686 wire machinery it serves; the `fastmcp-tasks` client half registers its own binding for the SEP-2663 `notifications/tasks` shape when it ships (push notifications are deferred to a later `fastmcp-tasks` version — v1 is polling-only).
|
||||
|
||||
## Statelessness on 2026-07-28
|
||||
|
||||
The `2026-07-28` era is stateless by protocol construction, and the recurring maintainer question is whether that statelessness has to be woven through FastMCP everywhere. It does not — but the honest accounting has three parts: features that are legacy-only because the protocol removed the mechanism, features that already work because they never relied on a session, and a short list of design holes where the current code *doesn't error* but also *doesn't work*. Everything below concerns `2026-07-28` connections only. Every client in the field today negotiates a handshake era, where all of this behaves exactly as it always has.
|
||||
|
||||
**The SDK ground truth.** On the modern paths the SDK's `Connection` is strictly per-request: a fresh `Connection` is built from each POST's envelope, its `exit_stack` unwinds when the request returns, `connection.session_id` is always `None`, and `connection.state` is a fresh dict per request. The manager's `stateless` flag never enters the picture — modern routing short-circuits ahead of it. There is no standing server→client stream: notifications emitted *during* a request ride that POST's own SSE sink, and anything emitted after the POST returns is dropped (`_NO_CHANNEL`); server→client *requests* raise `NoBackChannelError`. The only replacement is `subscriptions/listen`, which carries four list-changed / resource-updated event kinds and nothing else — no logging, progress, or task-status events, no resumability, and it is not yet wired into FastMCP. There is no `EventStore` or `Last-Event-ID` on modern paths at all; both belong to the legacy transport.
|
||||
|
||||
### Legacy-only by construction — document, don't build
|
||||
|
||||
These are not bugs. The protocol removed the mechanism they depend on, so they are simply out of scope on `2026-07-28`:
|
||||
|
||||
- **Per-session log levels.** `logging/setLevel` is absent from the 2026 method registry, so the `_client_log_levels` handler is unreachable. There is no per-session log-level state because there is no session.
|
||||
- **`EventStore` / resumability.** `EventStore`, `SessionScopedEventStore`, and Last-Event-ID resumption are never constructed on the modern paths. Resumability presupposes a durable stream, which the era does not have.
|
||||
- **Ping keepalive.** Server-initiated ping is a server→client request and is therefore structurally a no-op on modern connections; the SDK owns SSE-level pings on this transport.
|
||||
|
||||
### Already stateless by construction — works on 2026
|
||||
|
||||
These work on `2026-07-28` today because they never leaned on a protocol session:
|
||||
|
||||
- **`tasks/get` polling.** Task result retrieval is keyed by `task_id` and backed by Docket/Redis, so a client polls across independent requests without any session affinity. This session-free polling is exactly why the execution engine survives the SEP-1686-to-SEP-2663 rework: the SEP-2663 wire shape (poll `tasks/get`, resolve in-task input via `tasks/update`) maps onto the same durable store, and SEP-2663's `Mcp-Name: <taskId>` routing header is moot for a shared-Redis deployment where any replica can serve the poll. See [the xfail register](#the-xfail-register).
|
||||
- **OAuth bearer validation.** Auth is per-request bearer validation — every POST carries and re-validates its own credential.
|
||||
- **In-request progress and logging notifications.** Notifications emitted while a request is still streaming ride that POST's SSE sink and are delivered normally.
|
||||
|
||||
### Design holes deferred to the multi-protocol workstream
|
||||
|
||||
The remaining items are real holes, deferred to the [first-class 2026 client](/development/v4-notes/feature-program#first-class-2026-client) workstream because they all reduce to one unanswered question — *what is a session when the protocol has none?* The danger in each is that the code currently returns without erroring, which reads as "works" but is actually silent degradation. Again: these affect `2026-07-28` connections only; on the handshake eras every one of them behaves correctly.
|
||||
|
||||
- **`ctx.session_id` and `ctx.set_state` / `ctx.get_state` (broken even single-replica).** On a modern request `ctx.session_id` mints a fresh `uuid4`, cached on the per-request `connection.state` that is discarded when the request returns. So `ctx.set_state` and `ctx.get_state` silently never round-trip across requests — no error, just lost data. The open design decision is whether `session_id` should become `None` with `set_state` documented as session-era-only, or be re-based on an app-level key (the auth subject, or a client-supplied header).
|
||||
- **Task push and in-task input — resolved by the SEP-2663 design, not a statelessness hole.** This was previously framed as a hole because SEP-1686 leaned on a push back-channel (the notification/elicitation relay) that dies once the submitting request returns. SEP-2663 removes the dependency: in-task input is *poll-based* — the task enters `input_required`, surfaces its outstanding elicit/sample/roots requests in an `inputRequests` map on `tasks/get`, and the client answers via `tasks/update`. That round-trips through the durable store with no session affinity, so it is stateless-safe by construction. The SEP-1686 push relay (`server/tasks/elicitation.py`, `notifications.py`) is removed; the `fastmcp-tasks` rebuild implements the poll-based channel instead. Foreground (non-task) elicitation on 2026 remains the guard-mode `InputRequiredResult`.
|
||||
- **Stateful proxy affinity (degraded).** The stateful proxy's `_caches` are keyed by the per-request `Connection`, so on modern connections the proxy collapses to stateless proxying: results stay correct, but the per-session affinity guarantee is lost. This is decided alongside the `session_id` question — same root — or gated to the legacy/stdio transports.
|
||||
|
||||
Multi-replica concerns (per-process rate-limiter buckets, shared Redis backends for state and tasks, a Redis `SubscriptionBus`) are deployment configuration rather than protocol gaps and are out of scope for this section.
|
||||
|
||||
## Upstream advisory dossier
|
||||
|
||||
FastMCP acts as an advisor to the SDK team. The migration produced a dossier of ten findings (`sdk-feedback.md`) — verified bugs and hard edges to report upstream, plus questions to bundle into a feedback thread. The highest-priority items:
|
||||
|
||||
- **#1 (bug)** — SEP-1686 task result types ship but the method registries omit them. *Moot: the SEP-1686 wire shape was removed from the spec; the SEP-2663 rebuild claims `CreateTaskResult` on `tools/call` through the extensions mechanism, which the registries already admit.*
|
||||
- **#2 (bug/question)** — `capabilities.extensions` stripped at pre-2026 negotiated versions. **Elevated:** this now gates the `io.modelcontextprotocol/tasks` extension (and MCP Apps) on the modern era, so it blocks a flagship v4 feature rather than an edge case. Worth prioritizing in the upstream thread.
|
||||
- **#4 (security)** — DCR redirect-URI validation accepts `javascript:`/`data:` schemes.
|
||||
- **#5 (hard edge)** — `streamable_http_client` drops session-id access with no replacement.
|
||||
- **#8 (hard edge)** — custom server notifications are dropped, not tee'd to `message_handler`.
|
||||
- **#10 (hard edge)** — 2026 push-feature degradation error quality is inconsistent. *Resolved on the FastMCP side: `ctx.elicit` / `ctx.sample` are era-gated to raise a clear error on modern connections (#4448).*
|
||||
|
||||
Filing is gated on maintainer approval of each issue text.
|
||||
|
||||
Separately, the [SDK delegation round two](/development/v4-notes/feature-program#sdk-delegation-round-two) work depends on **three upstream feature requests** — per-session event-store scoping, a user-middleware injection hook, and a lifespan hook — that would let FastMCP collapse its HTTP builders onto the SDK's and inherit the SDK's session-owner credential enforcement.
|
||||
|
||||
## GA transition checklist
|
||||
|
||||
The beta-to-stable transition is a small set of tracked steps:
|
||||
|
||||
- **Swap the pins.** When `mcp 2.0.0` reaches GA, change `mcp-types==2.0.0b1` (core) and the `mcp` pin (the `[mcp]` extra) in `fastmcp_slim/pyproject.toml` from the beta to the stable release, and cut `4.0.0` instead of another pre-release.
|
||||
- **Re-run the xfail suite against the GA SDK.** Any strict xfail that starts passing means a gap closed — remove the marker and, where applicable, the corresponding shim.
|
||||
- **Confirm `release/3.x`** is cut from pre-merge `main` and receiving upstream security patches for users who stay on the SDK v1 line.
|
||||
53
docs/development/v4-notes/protocol-2026.mdx
Normal file
53
docs/development/v4-notes/protocol-2026.mdx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
title: 2026-07-28 Protocol Support
|
||||
---
|
||||
|
||||
FastMCP v4 serves the sessionless `2026-07-28` protocol era and the session-based handshake eras from a single server, with per-connection auto-detection. This page catalogs what FastMCP provides for the modern era — both the protocol machinery it inherits from the MCP Python SDK and the capabilities FastMCP implements itself on top of that layer. It is the reference for what a v4 deployment can actually do on the modern protocol today.
|
||||
|
||||
## Identity assertion (SEP-990)
|
||||
|
||||
SEP-990 defines enterprise "on-behalf-of" access: a corporate identity provider (Okta, Microsoft Entra, etc.) issues a signed *ID-JAG* asserting an employee's identity, the employee's agent presents it at the MCP authorization server's token endpoint via the RFC 7523 `jwt-bearer` grant, and receives a short-lived access token — no browser login, no per-user consent screen, and revocation lives at the IdP.
|
||||
|
||||
The protocol layer for this flow — grant parsing, the `exchange_identity_assertion` provider hook, and metadata advertisement — comes from the SDK. The validation and issuance logic that makes the flow actually work is FastMCP's implementation, and enabling it is one parameter on the existing auth providers:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth import OAuthProxy, IdentityAssertion
|
||||
|
||||
auth = OAuthProxy(
|
||||
..., # existing upstream configuration unchanged
|
||||
identity_assertion=IdentityAssertion(
|
||||
trusted_issuers=["https://login.acme-corp.com"],
|
||||
),
|
||||
)
|
||||
mcp = FastMCP("Internal API", auth=auth)
|
||||
```
|
||||
|
||||
Behind that one parameter, FastMCP performs the full SEP-990 §5.1 / RFC 7523 §3 processing: JWKS-based signature verification with automatic OIDC discovery of issuer keys, `typ`/`iss`/`aud`/`sub` validation, temporal checks (`exp`, `iat`, `nbf`, maximum assertion lifetime), enforcement of the assertion's signed `client_id` and `resource` bindings, `jti` replay rejection, scope derivation from the signed assertion (client requests can narrow but never widen), short-lived token issuance with no refresh token, and revocation tracking for the issued tokens. The asserted subject flows into the normal FastMCP auth context, so tools read it through `get_access_token()` like any other identity. See [Identity Assertion](/servers/auth/oauth-proxy#identity-assertion-sep-990) for the full documentation.
|
||||
|
||||
This slots into FastMCP's existing authorization-server stack — the OAuth proxy's dynamic client registration, the consent flow, and self-issued JWTs — which is what makes a one-parameter enterprise deployment possible.
|
||||
|
||||
## Modern-era capability inventory
|
||||
|
||||
The complete picture of what a FastMCP v4 server and client provide on the `2026-07-28` era:
|
||||
|
||||
| Capability | What FastMCP provides |
|
||||
| --- | --- |
|
||||
| **Dual-era serving** | One server answers both `server/discover` (modern, sessionless) and `initialize` (handshake) connections, auto-detected per connection. Any replica behind a plain load balancer can answer a modern request. |
|
||||
| **Identity assertion (SEP-990)** | Complete server-side implementation, one parameter to enable (above). |
|
||||
| **Authorization server** | Full AS stack: `OAuthProxy` bridges DCR-expecting MCP clients to non-DCR enterprise IdPs, ~18 built-in providers, consent UI, self-issued JWTs, protected-resource metadata (RFC 9728). |
|
||||
| **Cache hints (SEP-2549)** | Server-level authoring (`FastMCP(cache_ttl=..., cache_scope=...)`) stamps every cacheable result; the FastMCP client honors hints with an opt-in response cache. |
|
||||
| **Distributed response caching** | `KeyValueResponseCacheStore` backs the client cache with any key-value store (Redis, memory, filetree), so a fleet of clients or proxy replicas shares cache fills across processes. |
|
||||
| **Resource path security** | Templated resource parameters are screened for traversal, absolute paths, and null bytes before handlers run — on by default, including provider-sourced and mounted templates. |
|
||||
| **Client protocol negotiation** | `Client(mode="auto")` — the default as of v4 — probes `server/discover` and falls back to the classic handshake; the client answers multi-round-trip `input_required` requests through its existing handlers. Pin `mode="legacy"` to force the handshake. |
|
||||
| **Elicitation on the modern protocol (SEP-2322)** | Tools request user input via multi-round trips: a tool returns an `InputRequiredResult` and re-runs per round, reading the client's answers off `ctx.input_responses` / `ctx.request_state` (the [guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol)). Each round is a complete request→response cycle; the framework seals `request_state` on the wire and unseals it before the tool runs, and a shared-key `request_state_security` policy carries state across replicas. On handshake-era connections returning this result produces a clear era error. |
|
||||
| **Spec-standard errors (SEP-2164)** | Missing-resource reads return `-32602`; push-feature calls on modern connections fail with clear era-specific errors rather than generic method-not-found. |
|
||||
| **Middleware** | Typed per-method hooks (`on_call_tool`, `on_list_tools`, …) and a suite of built-ins (auth, rate limiting, caching, error handling, logging, timing, and more). |
|
||||
| **Composition** | `mount()`, providers, proxying, and tool transforms compose servers dynamically at runtime, with lifespans and middleware driven through the SDK session manager. |
|
||||
| **Pagination** | Declarative `FastMCP(list_page_size=...)` paginates all list operations in the high-level server; the client auto-paginates with cycle detection. |
|
||||
| **Telemetry** | OpenTelemetry spans on by default (no-op without an exporter), SDK-aligned attributes (`mcp.method.name`, `mcp.protocol.version`, `gen_ai.*`), plus auth and provider-delegation spans; `FASTMCP_ENABLE_TELEMETRY=false` disables cleanly. |
|
||||
| **Background tasks (SEP-2663)** | `fastmcp-tasks` implements the `io.modelcontextprotocol/tasks` extension end to end: `mcp.add_extension(TasksExtension())` plus `task=True` runs a tool as a background task, driven by the same Docket engine FastMCP 3 used. A client transparently completes a tasked call; gathering input mid-task uses the same guard pattern as foreground multi-round-trip tools, so a tool is written once and works either way. Modern-protocol only — the `task=True` runtime this replaced (SEP-1686) is gone entirely, not bridged. See [Background Tasks (SEP-2663)](/development/v4-notes/background-tasks) for the design and [servers/tasks](/servers/tasks) for usage. |
|
||||
|
||||
## Still in the program
|
||||
|
||||
Elicitation on the modern protocol is now shipped in its **guard form** — a tool returns an `InputRequiredResult` and re-runs per round to gather user input via multi-round trips (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)). The declarative `Resolve(...)` layer over that primitive remains staged, tracked in the [Feature Program](/development/v4-notes/feature-program), along with the unified `subscriptions/listen` stream. The [Known Gaps](/development/v4-notes/known-gaps) page tracks the upstream dependencies that gate them.
|
||||
217
docs/development/v4-notes/stateless-session-state.md
Normal file
217
docs/development/v4-notes/stateless-session-state.md
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
# Stateless session state (2026-07-28)
|
||||
|
||||
> Design spec. Status: building.
|
||||
|
||||
## Problem
|
||||
|
||||
The `2026-07-28` era is stateless by protocol construction: each request builds a
|
||||
fresh `Connection`, `connection.session_id` is always `None`, and
|
||||
`connection.state` is a new dict discarded when the request returns. So
|
||||
`ctx.session_id` mints a throwaway `uuid4` per request and `ctx.set_state` /
|
||||
`ctx.get_state` **silently never round-trip** — no error, just lost data. A user
|
||||
who wants cross-call state (a cart, a conversation, accumulated context) has no
|
||||
safe mechanism, and the failure is invisible.
|
||||
|
||||
The one identifier every modern request carries that is stable and
|
||||
**non-spoofable** is the authenticated principal — `get_access_token().claims["sub"]`,
|
||||
or the `(client_id, issuer, subject)` triple. Everything else on the wire is
|
||||
client-declared and forgeable.
|
||||
|
||||
## The model
|
||||
|
||||
State lives **server-side** in the one `AsyncKeyValue` (py-key-value) store the
|
||||
server already holds (`session_state_store`). The framework calls `get`/`put`/
|
||||
`delete` and **never imposes a TTL** — retention is entirely the store's
|
||||
(configure it on the store you pass: a Redis TTL, a py-key-value TTL wrapper,
|
||||
whatever). There is no second store and no framework-owned TTL knob.
|
||||
|
||||
Isolation comes from the **authenticated principal, not from the session id.**
|
||||
State is keyed by `(principal, session_id)`. A request under principal B keys
|
||||
into B's own namespace — it can never address A's keys no matter what
|
||||
`session_id` it passes. The id only organizes sessions *within* a principal. The
|
||||
handle is a bare `uuid4` string; it is **not sealed** — the principal prefix is
|
||||
the wall. Sessions are also create-then-validate (below): an id that was never
|
||||
minted by `create_session` under this principal is rejected outright, not
|
||||
resolved to an empty session.
|
||||
|
||||
## Two explicit patterns
|
||||
|
||||
A tool opts into exactly one, on purpose. There is deliberately **no** optional
|
||||
"id if given, else default" parameter — that would silently misroute a call
|
||||
whose id the agent forgot to pass into the shared per-user bucket, which is the
|
||||
invisible-degradation failure this whole feature exists to remove.
|
||||
|
||||
### Per-user state — injected
|
||||
|
||||
```python
|
||||
from fastmcp.server.sessions import UserSession
|
||||
|
||||
@mcp.tool
|
||||
async def remember(fact: str, session: UserSession) -> str:
|
||||
await session.set("fact", fact)
|
||||
return "noted"
|
||||
```
|
||||
|
||||
`session: UserSession` is **dependency-injected** (like `ctx: Context`): keyed by
|
||||
the request's authenticated principal, not present in the input schema, nothing
|
||||
for the agent to pass. Requires auth — with no principal it raises a clear error.
|
||||
Use it when one bucket per user is what you want. `UserSession` is only the
|
||||
injection annotation — the value the handler receives is an ordinary `Session`,
|
||||
so its `get`/`set`/`delete`/`clear` accessors work as usual.
|
||||
|
||||
### Distinct sessions — an argument
|
||||
|
||||
```python
|
||||
from fastmcp.server.sessions import SessionId
|
||||
from fastmcp.server.dependencies import get_session
|
||||
|
||||
@mcp.tool
|
||||
async def add_to_cart(item: str, session_id: SessionId) -> str:
|
||||
session = await get_session(session_id)
|
||||
cart = await session.get("cart", default=[])
|
||||
cart.append(item)
|
||||
await session.set("cart", cart)
|
||||
return f"{len(cart)} items"
|
||||
```
|
||||
|
||||
`session_id: SessionId` is a **required string argument** — it *is* in the schema,
|
||||
the agent supplies it. `SessionId` is a marker type so the framework
|
||||
auto-populates the argument's description with the protocol:
|
||||
|
||||
> "Session identifier. Use a tool to create a session, then pass the resulting id
|
||||
> here to persist state across calls in the same session."
|
||||
|
||||
The tool becomes self-teaching — an agent reads the schema and learns the
|
||||
create-then-pass contract with no hand-prompting. The description names no
|
||||
specific tool: composition can rename the lifecycle tool (mounting under a
|
||||
namespace exposes it as `child_create_session`), so it points at the
|
||||
*capability* rather than a name that may not exist under that mount.
|
||||
|
||||
The standalone `await get_session(session_id)` resolves the id to a `Session`
|
||||
keyed by `(principal, session_id)`, **validating** that it was created under this
|
||||
principal — an unknown or foreign id raises `InvalidSession` rather than opening a
|
||||
fresh bucket. It is a plain function, not a `Context` method, so it needs no
|
||||
foreground context and works from a `task=True` tool's worker. Use this pattern
|
||||
when a user needs more than one session.
|
||||
|
||||
## The `Session` object
|
||||
|
||||
Async accessors over the server store, scoped to one `(principal, session_id)`:
|
||||
|
||||
- `session.id` — the session's id (set for a `session_id`-resolved session; `None`
|
||||
for an injected `UserSession`, which has no distinct id).
|
||||
- `await session.get(key, default=None)`
|
||||
- `await session.set(key, value)`
|
||||
- `await session.delete(key)`
|
||||
- `await session.clear()` — empties user state but **keeps the session valid**.
|
||||
- `await session.end()` — deletes the session (what `end_session` calls).
|
||||
|
||||
A session's state is stored as a **single dict under one key**
|
||||
(`session:{sha256(principal)}:{session_id}`, and `session:anon:{session_id}` when
|
||||
unauthenticated — the principal is hashed into a fixed-length, delimiter-safe
|
||||
segment, never embedded raw). That dict holds user state in a `state` sub-dict
|
||||
alongside a small `_created` marker, so a created-but-empty session is
|
||||
distinguishable from a missing one even if the store collapses empty dicts.
|
||||
`get`/`set`/`delete` read-modify-write the sub-dict and never touch the marker;
|
||||
`clear` resets the sub-dict but leaves the marker (the session still resolves);
|
||||
`end` deletes the key. Namespacing user state under `state` is what keeps a user
|
||||
key named `_created` from colliding with the marker. One key per session means
|
||||
one TTL per session (the store's), refreshed on write — no key index to maintain,
|
||||
and `end` is a single delete. (Trade-off: concurrent writes to one session race
|
||||
on the read-modify-write; session state is small and typically driven serially by
|
||||
one agent, so this is acceptable — noted, not hidden.)
|
||||
|
||||
## `SessionProvider`
|
||||
|
||||
Session ids are minted by `SessionProvider`, which contributes two tools:
|
||||
|
||||
- `create_session()` → mints an unguessable `uuid4`, **records** the session
|
||||
under the current principal, and returns the id as a string.
|
||||
- `end_session(session_id: SessionId)` → validates the id, then deletes the
|
||||
session so it no longer resolves.
|
||||
|
||||
Register it whenever your tools take a `session_id` — providers are the idiomatic
|
||||
way to add functionality like this:
|
||||
|
||||
```python
|
||||
from fastmcp.server.sessions import SessionProvider
|
||||
|
||||
mcp.add_provider(SessionProvider())
|
||||
```
|
||||
|
||||
There is **no enforcement** that a provider is registered, and there was: an
|
||||
earlier version scanned the tool set at list/resolve time and raised if a
|
||||
`session_id` tool had no provider. That check had to reason about the whole
|
||||
composition pipeline — `isinstance` on providers, unwrapping namespaced ones,
|
||||
tool transforms, session visibility, enabled state — and produced false
|
||||
positives that broke valid servers (a namespaced provider, a session-disabled
|
||||
tool). It was deleted. The guarantee never needed it: `get_session` validates
|
||||
that an id was recorded (create-then-validate), so a server with no provider
|
||||
simply cannot mint ids, and every `get_session` rejects — a misconfiguration
|
||||
caught the first time the tools run, not a security hole.
|
||||
|
||||
`SessionProvider` subclasses `Provider`, takes **no store** (uses the server's)
|
||||
and **no ttl** (the store's). It exists to mint and end owned ids.
|
||||
`create_session` matters most without auth, where an unguessable id is the only
|
||||
defense against a caller *guessing* onto another session.
|
||||
|
||||
When an application already mints its own identifiers — conversation ids, workflow
|
||||
ids — take them as ordinary string arguments rather than `SessionId`, and register
|
||||
no provider; `SessionId` is specifically the create-then-pass contract backed by
|
||||
`create_session`.
|
||||
|
||||
## Security
|
||||
|
||||
Keyed by `(principal, session_id)`:
|
||||
|
||||
- **Authenticated → strong isolation.** `principal` is the validated token
|
||||
subject, unforgeable. B keys into B's namespace; A's data is unreachable no
|
||||
matter what id B passes. Guessing is pointless; a session id appearing in agent
|
||||
context or logs is harmless (it is not a capability without the principal).
|
||||
Caller-chosen ids are safe here.
|
||||
- **Unauthenticated → single-tenant-safe only.** No principal, so the key is just
|
||||
the id in a shared namespace: the id becomes a bearer capability, and exposure
|
||||
in logs/conversation leaks the session. `create_session`'s `uuid4` gives
|
||||
guess-*resistance*, not isolation. Documented in bold: not a tenant boundary;
|
||||
without auth, force minted ids and never treat sessions as a wall between
|
||||
clients.
|
||||
- **Isolation is auth; the id is organization.** No id scheme substitutes for a
|
||||
principal, which is why sealing the handle buys nothing load-bearing and is
|
||||
dropped.
|
||||
- **Not FastMCP's job:** transport (use TLS), encryption at rest (the store's), a
|
||||
malicious *authorized* client acting within its rights.
|
||||
|
||||
## Rework plan (from the current prototype)
|
||||
|
||||
The prototype (`sessions.py`, `context.py`, `function_tool.py`, `server.py`) built
|
||||
a `Scope` enum, a sealed `SessionCodec`, and `ctx.get_state(scope=...)`. Rework to
|
||||
the above:
|
||||
|
||||
1. **Remove `Scope`** and the `scope=` parameter; revert `ctx.get_state`/
|
||||
`set_state` to their original request-scoped behavior.
|
||||
2. **Remove the `SessionCodec`/sealing** — ids are bare `uuid4`.
|
||||
3. **`Session` object** with async `get`/`set`/`delete`/`clear` over the server
|
||||
store, single-dict-per-session key scheme.
|
||||
4. **`session: UserSession`** injection (principal-keyed; error without auth) —
|
||||
wire into the same parameter-detection path as `Context`. `UserSession` is the
|
||||
injection marker; the injected value is a `Session`.
|
||||
5. **`session_id: SessionId`** marker type: string in the schema, auto-filled
|
||||
description, standalone `await get_session(id)` resolver that validates the id
|
||||
(works from a task worker — no foreground context needed).
|
||||
6. **`SessionProvider(Provider)`** with `create_session` (records the session) /
|
||||
`end_session` (deletes it), registered explicitly via `add_provider`. No
|
||||
enforcement that it is present — `get_session`'s validation is the guarantee.
|
||||
7. Rewrite the tests to cover both patterns, principal isolation, no-auth
|
||||
behavior, and `end_session`.
|
||||
|
||||
## Docs plan
|
||||
|
||||
Written against the final API once the rework verifies:
|
||||
|
||||
- A concept guide — why stateless removes the session, the two patterns, when to
|
||||
reach for each. Why before how.
|
||||
- A security page — the two tiers, "isolation is auth, the id is organization,"
|
||||
the bold no-multitenant-without-auth warning.
|
||||
- Fully runnable examples for both patterns (pass the doc-import guard, register
|
||||
in `docs.json`).
|
||||
- A migration note from the old `ctx.session_id` / `set_state`.
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
"dark": "#475569",
|
||||
"light": "#1e3a5f"
|
||||
},
|
||||
"content": "Meet [Prefect Horizon](https://prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_banner&utm_content=sitewide_banner), the enterprise MCP gateway built by the team behind FastMCP"
|
||||
"content": "FastMCP 4 is in alpha — you're reading the v4 docs. [What's new](/getting-started/whats-new) · [FastMCP 3 docs](/v3/getting-started/welcome)"
|
||||
},
|
||||
"colors": {
|
||||
"dark": "#f72585",
|
||||
|
|
@ -89,7 +89,8 @@
|
|||
"pages": [
|
||||
"getting-started/welcome",
|
||||
"getting-started/installation",
|
||||
"getting-started/quickstart"
|
||||
"getting-started/quickstart",
|
||||
"getting-started/whats-new"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -144,6 +145,7 @@
|
|||
"pages": [
|
||||
"servers/elicitation",
|
||||
"servers/sampling",
|
||||
"servers/completions",
|
||||
"servers/progress",
|
||||
"servers/logging",
|
||||
"servers/pagination",
|
||||
|
|
@ -159,6 +161,7 @@
|
|||
"servers/dependency-injection",
|
||||
"servers/lifespan",
|
||||
"servers/storage-backends",
|
||||
"servers/sessions",
|
||||
"servers/tasks",
|
||||
"servers/versioning"
|
||||
]
|
||||
|
|
@ -264,6 +267,7 @@
|
|||
"icon": "key",
|
||||
"pages": [
|
||||
"clients/auth/oauth",
|
||||
"clients/auth/client-credentials",
|
||||
"clients/auth/cimd",
|
||||
"clients/auth/bearer"
|
||||
],
|
||||
|
|
@ -288,6 +292,7 @@
|
|||
"integrations/eunomia-authorization",
|
||||
"integrations/github",
|
||||
"integrations/google",
|
||||
"integrations/huggingface",
|
||||
"integrations/keycloak",
|
||||
"integrations/oci",
|
||||
"integrations/permit",
|
||||
|
|
@ -354,6 +359,7 @@
|
|||
"icon": "up",
|
||||
"pages": [
|
||||
"getting-started/upgrading/from-fastmcp-2",
|
||||
"getting-started/upgrading/from-fastmcp-3",
|
||||
"getting-started/upgrading/from-mcp-sdk",
|
||||
"getting-started/upgrading/from-low-level-sdk"
|
||||
]
|
||||
|
|
@ -366,7 +372,19 @@
|
|||
"development/contributing",
|
||||
"development/tests",
|
||||
"development/releases",
|
||||
"patterns/contrib"
|
||||
"patterns/contrib",
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "v4 Notes",
|
||||
"pages": [
|
||||
"development/v4-notes/index",
|
||||
"development/v4-notes/change-register",
|
||||
"development/v4-notes/feature-program",
|
||||
"development/v4-notes/background-tasks",
|
||||
"development/v4-notes/protocol-2026",
|
||||
"development/v4-notes/known-gaps"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -395,7 +413,10 @@
|
|||
"icon": "code"
|
||||
}
|
||||
],
|
||||
"version": "v3"
|
||||
"version": "v4.0.0 (alpha 1)"
|
||||
},
|
||||
{
|
||||
"$ref": "./v3-navigation.json"
|
||||
},
|
||||
{
|
||||
"$ref": "./v2-navigation.json"
|
||||
|
|
@ -482,6 +503,10 @@
|
|||
{
|
||||
"destination": "/getting-started/upgrading/from-low-level-sdk",
|
||||
"source": "/getting-started/low-level-sdk"
|
||||
},
|
||||
{
|
||||
"destination": "/getting-started/upgrading/from-fastmcp-3",
|
||||
"source": "/getting-started/upgrading/to-mcp-sdk-v2"
|
||||
}
|
||||
],
|
||||
"search": {
|
||||
|
|
|
|||
|
|
@ -68,20 +68,20 @@ BREAKING CHANGES (will crash at import or runtime):
|
|||
|
||||
6. WSTRANSPORT: Removed. Use StreamableHttpTransport.
|
||||
|
||||
7. OPENAPI: timeout parameter removed from OpenAPIProvider. Set timeout on the httpx.AsyncClient instead.
|
||||
7. OPENAPI: timeout parameter removed from OpenAPIProvider. Set timeout on the httpx2.AsyncClient instead.
|
||||
|
||||
8. METADATA: Namespace changed from "_fastmcp" to "fastmcp" in tool.meta. The include_fastmcp_meta parameter is removed (always included).
|
||||
|
||||
9. ENV VAR: FASTMCP_SHOW_CLI_BANNER renamed to FASTMCP_SHOW_SERVER_BANNER.
|
||||
|
||||
10. DECORATORS: @mcp.tool, @mcp.resource, @mcp.prompt now return the original function, not a component object. Code that accesses .name, .description, or other component attributes on the decorated result will crash with AttributeError.
|
||||
Fix: set FASTMCP_DECORATOR_MODE=object for v2 compat (itself deprecated).
|
||||
Fix: access component objects via the server (e.g. await mcp.get_tool("name")) instead of the decorated function. The FASTMCP_DECORATOR_MODE=object escape hatch that existed in v3 was removed in FastMCP 4.0.
|
||||
|
||||
11. OAUTH STORAGE: Default OAuth client storage changed from DiskStore to FileTreeStore due to pickle deserialization vulnerability in diskcache (CVE-2025-69872). Clients using default storage will re-register automatically on first connection. If using DiskStore explicitly, switch to FileTreeStore (with key/collection sanitization strategies) or add pip install 'py-key-value-aio[disk]'.
|
||||
|
||||
12. REPO MOVE: GitHub repository moved from jlowin/fastmcp to PrefectHQ/fastmcp. Update git remotes and dependency URLs that reference the old location.
|
||||
|
||||
13. BACKGROUND TASKS: FastMCP's background task system (SEP-1686) is now an optional dependency. If the code uses task=True or TaskConfig, add pip install "fastmcp[tasks]".
|
||||
13. BACKGROUND TASKS: FastMCP's background task system is now an optional dependency. If the code uses task=True or TaskConfig, add pip install "fastmcp[tasks]".
|
||||
|
||||
DEPRECATIONS (still work but emit warnings):
|
||||
|
||||
|
|
@ -276,14 +276,14 @@ transport = StreamableHttpTransport("http://localhost:8000/mcp")
|
|||
|
||||
**OpenAPI `timeout` parameter removed**
|
||||
|
||||
`OpenAPIProvider` no longer accepts a `timeout` parameter. Configure timeout on the httpx client directly. The `client` parameter is also now optional — when omitted, a default client is created from the spec's `servers` URL with a 30-second timeout:
|
||||
`OpenAPIProvider` no longer accepts a `timeout` parameter. Configure timeout on the httpx2 client directly. The `client` parameter is also now optional — when omitted, a default client is created from the spec's `servers` URL with a 30-second timeout:
|
||||
|
||||
```python
|
||||
# Before
|
||||
provider = OpenAPIProvider(spec, client, timeout=60)
|
||||
|
||||
# After
|
||||
client = httpx.AsyncClient(base_url="https://api.example.com", timeout=60)
|
||||
client = httpx2.AsyncClient(base_url="https://api.example.com", timeout=60)
|
||||
provider = OpenAPIProvider(spec, client)
|
||||
```
|
||||
|
||||
|
|
@ -317,11 +317,11 @@ def greet(name: str) -> str:
|
|||
greet("World") # Works! Returns "Hello, World!"
|
||||
```
|
||||
|
||||
If you have code that treats the decorated result as a `FunctionTool` (e.g., accessing `.name` or `.description`), set `FASTMCP_DECORATOR_MODE=object` for v2 compatibility. This escape hatch is itself deprecated and will be removed in a future release.
|
||||
If you have code that treats the decorated result as a `FunctionTool` (e.g., accessing `.name` or `.description`), the v2-compatible object-returning behavior was available in v3 via `FASTMCP_DECORATOR_MODE=object`. That escape hatch was removed in FastMCP 4.0 — decorators always return the original function now.
|
||||
|
||||
**Background tasks require optional dependency**
|
||||
|
||||
FastMCP's background task system (SEP-1686) is now behind an optional extra. If your server uses background tasks, install with:
|
||||
FastMCP's background task system is now behind an optional extra. If your server uses background tasks, install with:
|
||||
|
||||
```bash
|
||||
pip install "fastmcp[tasks]"
|
||||
|
|
@ -331,22 +331,22 @@ Without the extra, configuring a tool with `task=True` or `TaskConfig` will rais
|
|||
|
||||
### Deprecated Features
|
||||
|
||||
These still work but emit warnings. Update when convenient.
|
||||
These were deprecated in v3. Items marked **Removed in v4** no longer work at all — update to the replacement shown. The rest still work but emit warnings; update when convenient.
|
||||
|
||||
**mount() prefix → namespace**
|
||||
**mount() prefix → namespace** (Removed in v4)
|
||||
|
||||
```python
|
||||
# Deprecated
|
||||
# Removed in v4
|
||||
main.mount(subserver, prefix="api")
|
||||
|
||||
# New
|
||||
main.mount(subserver, namespace="api")
|
||||
```
|
||||
|
||||
**import_server() → mount()**
|
||||
**import_server() → mount()** (Removed in v4)
|
||||
|
||||
```python
|
||||
# Deprecated
|
||||
# Removed in v4
|
||||
main.import_server(subserver)
|
||||
|
||||
# New
|
||||
|
|
@ -355,10 +355,10 @@ main.mount(subserver)
|
|||
|
||||
**Module import paths for proxy and OpenAPI**
|
||||
|
||||
The proxy and OpenAPI modules have moved under `providers` to reflect v3's provider-based architecture:
|
||||
The proxy and OpenAPI modules moved under `providers` to reflect v3's provider-based architecture. The old `fastmcp.server.proxy` and `fastmcp.server.openapi` compatibility shims were **removed in 4.0** — import from the `providers` location instead:
|
||||
|
||||
```python test="skip"
|
||||
# Deprecated
|
||||
# Removed in 4.0
|
||||
from fastmcp.server.proxy import FastMCPProxy
|
||||
from fastmcp.server.openapi import FastMCPOpenAPI
|
||||
|
||||
|
|
@ -367,10 +367,10 @@ from fastmcp.server.providers.proxy import FastMCPProxy
|
|||
from fastmcp.server.providers.openapi import OpenAPIProvider
|
||||
```
|
||||
|
||||
`FastMCPOpenAPI` itself is deprecated — use `FastMCP` with an `OpenAPIProvider` instead:
|
||||
`FastMCPOpenAPI` was **removed in 4.0** — use `FastMCP` with an `OpenAPIProvider` instead:
|
||||
|
||||
```python test="skip"
|
||||
# Deprecated
|
||||
# Removed in 4.0
|
||||
from fastmcp.server.openapi import FastMCPOpenAPI
|
||||
server = FastMCPOpenAPI(spec, client)
|
||||
|
||||
|
|
@ -380,10 +380,10 @@ from fastmcp.server.providers.openapi import OpenAPIProvider
|
|||
server = FastMCP("my_api", providers=[OpenAPIProvider(spec, client)])
|
||||
```
|
||||
|
||||
**add_tool_transformation() → add_transform()**
|
||||
**add_tool_transformation() → add_transform()** (Removed in v4)
|
||||
|
||||
```python
|
||||
# Deprecated
|
||||
# Removed in v4
|
||||
mcp.add_tool_transformation("name", config)
|
||||
|
||||
# New
|
||||
|
|
@ -391,29 +391,35 @@ from fastmcp.server.transforms import ToolTransform
|
|||
mcp.add_transform(ToolTransform({"name": config}))
|
||||
```
|
||||
|
||||
**FastMCP.as_proxy() → create_proxy()**
|
||||
**FastMCP.as_proxy() → create_proxy()** (Removed in v4)
|
||||
|
||||
The proxy target is passed positionally in both APIs, so most calls migrate unchanged. If you passed the target by keyword, note that the parameter was renamed from `backend=` to `target=`.
|
||||
|
||||
```python
|
||||
# Deprecated
|
||||
# Removed in v4
|
||||
proxy = FastMCP.as_proxy("http://example.com/mcp")
|
||||
proxy = FastMCP.as_proxy(backend="http://example.com/mcp") # keyword form
|
||||
|
||||
# New
|
||||
from fastmcp.server import create_proxy
|
||||
proxy = create_proxy("http://example.com/mcp")
|
||||
proxy = create_proxy(target="http://example.com/mcp") # as_proxy(backend=X) → create_proxy(target=X)
|
||||
```
|
||||
|
||||
## v2.14.0
|
||||
|
||||
### OpenAPI Parser Promotion
|
||||
|
||||
The experimental OpenAPI parser is now standard. Update imports:
|
||||
The experimental OpenAPI parser is now standard. The `fastmcp.experimental.server.openapi` and `fastmcp.server.openapi` shims were both **removed in 4.0** — use `FastMCP` with an `OpenAPIProvider` instead:
|
||||
|
||||
```python test="skip"
|
||||
# Before
|
||||
from fastmcp.experimental.server.openapi import FastMCPOpenAPI
|
||||
|
||||
# After
|
||||
from fastmcp.server.openapi import FastMCPOpenAPI
|
||||
# After (removed in 4.0 — use OpenAPIProvider)
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.providers.openapi import OpenAPIProvider
|
||||
server = FastMCP("my_api", providers=[OpenAPIProvider(spec, client)])
|
||||
```
|
||||
|
||||
### Removed Deprecated Features
|
||||
|
|
|
|||
275
docs/getting-started/upgrading/from-fastmcp-3.mdx
Normal file
275
docs/getting-started/upgrading/from-fastmcp-3.mdx
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
---
|
||||
title: Upgrading from FastMCP 3
|
||||
sidebarTitle: "From FastMCP 3.x"
|
||||
description: What changes when you upgrade to FastMCP 4, which builds on the MCP Python SDK v2
|
||||
icon: up
|
||||
---
|
||||
|
||||
FastMCP 4 builds on the MCP Python SDK v2, and that is the source of every change in this guide. The SDK v2 makes two sweeping changes to the protocol layer: it splits the protocol types out of `mcp.types` into a standalone `mcp_types` package, and it renames every protocol field from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, `isError` → `is_error`, and so on).
|
||||
|
||||
FastMCP 4 absorbs almost all of this for you. Field access is bridged so your existing reads keep working, and the imports you were taught have a stable home in FastMCP itself. The sections below describe what FastMCP handles for you, the small number of changes you must make in your own code, and the deprecation timeline for the compatibility shims.
|
||||
|
||||
## Install the v4 prerelease
|
||||
|
||||
While FastMCP 4 is in prerelease, pin the alpha and its prerelease protocol dependencies explicitly. For a uv project, add the following to `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[project]
|
||||
dependencies = ["fastmcp==4.0.0a1"]
|
||||
|
||||
[tool.uv]
|
||||
constraint-dependencies = [
|
||||
"fastmcp-slim==4.0.0a1",
|
||||
"mcp==2.0.0b2",
|
||||
"mcp-types==2.0.0b2",
|
||||
]
|
||||
```
|
||||
|
||||
Then run `uv lock` or `uv sync` normally. The constraints opt only these transitive packages into their prerelease versions; you do not need `--prerelease allow`, which permits prereleases throughout the dependency graph.
|
||||
|
||||
## Environment requirements
|
||||
|
||||
The SDK v2 raises FastMCP's dependency floors, which matters before any of your code runs.
|
||||
|
||||
**pydantic >= 2.12 is now the floor.** If your project pins an older pydantic (for example `pydantic==2.11.*`), installing this FastMCP release fails with an unsatisfiable-resolution error from your installer — bump your pin to `>=2.12` first. If you don't pin pydantic at all, installers upgrade it silently as part of the FastMCP upgrade.
|
||||
|
||||
**The server extra floors Starlette >= 1.0.1.** Modern FastAPI (0.11x and later) already runs on Starlette 1.x, so mounting a FastMCP server inside a FastAPI app coexists cleanly — verified with FastAPI 0.138.2. Only very old FastAPI versions pinned below Starlette 1.0.1 conflict; upgrade FastAPI if your resolver complains about Starlette.
|
||||
|
||||
## What FastMCP absorbs
|
||||
|
||||
### Legacy camelCase field access keeps working
|
||||
|
||||
Objects that FastMCP hands back to you — the results of `client.list_tools()`, `client.call_tool_mcp()`, `client.read_resource()`, and the parameter objects passed to your sampling and elicitation handlers — are SDK v2 objects with snake_case fields. FastMCP installs a compatibility bridge at import time that routes the old camelCase names to their new snake_case fields, so code written against FastMCP 2.x still reads correctly:
|
||||
|
||||
```python
|
||||
from fastmcp import Client
|
||||
|
||||
async with Client("my_mcp_server.py") as client:
|
||||
tools = await client.list_tools()
|
||||
schema = tools[0].inputSchema # still works, warns once
|
||||
```
|
||||
|
||||
Each bridged read emits a `FastMCPDeprecationWarning` pointing you at the snake_case name (`tools[0].input_schema` here). The bridge covers the fields users actually read: `inputSchema`/`outputSchema` on tools; `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint` on tool annotations; `mimeType` on resources and content; `isError`/`structuredContent` on tool results; `nextCursor` on paginated results; `serverInfo`/`protocolVersion` on the initialize result; the sampling parameter fields (`systemPrompt`, `maxTokens`, `stopSequences`, `modelPreferences`, `toolChoice`); and `requestedSchema` on elicitation parameters.
|
||||
|
||||
The bridge is controlled by the `mcp_camelcase_compat` setting, which defaults to on. Set it to `False` (or the environment variable `FASTMCP_MCP_CAMELCASE_COMPAT=false`) to turn the shims off, in which case only the snake_case names resolve:
|
||||
|
||||
```python
|
||||
import fastmcp
|
||||
|
||||
fastmcp.settings.mcp_camelcase_compat = False
|
||||
```
|
||||
|
||||
See [Settings](/more/settings) for the full reference.
|
||||
|
||||
### Protocol types moved to `mcp_types`
|
||||
|
||||
The `mcp.types` module no longer exists. Every protocol type — `TextContent`, `ImageContent`, `Tool`, `ErrorData`, `Icon`, `PromptMessage`, `SamplingMessage`, `ToolAnnotations`, notification and request wrapper types like `ToolListChangedNotification`, and everything else — now lives in the standalone `mcp_types` package. Update your imports to point there:
|
||||
|
||||
```python
|
||||
from mcp_types import TextContent, Tool, ToolAnnotations
|
||||
```
|
||||
|
||||
`fastmcp.types` still exists, but holds only types FastMCP defines itself (currently just `Textarea`, used to render a multiline textarea in form-based UIs) — it does not re-export protocol types.
|
||||
|
||||
### `McpError` has an alias
|
||||
|
||||
`fastmcp.exceptions.McpError` is an alias of the SDK's `MCPError`. Catching errors is unchanged — `except McpError` still catches SDK-raised errors, and reading `err.error.code` still works:
|
||||
|
||||
```python
|
||||
from fastmcp.exceptions import McpError
|
||||
|
||||
try:
|
||||
...
|
||||
except McpError as err:
|
||||
print(err.error.code)
|
||||
```
|
||||
|
||||
### Behavior preserved across the SDK boundary
|
||||
|
||||
A few client behaviors that touch the SDK are preserved so you don't have to change anything:
|
||||
|
||||
- `Client(timeout=...)` accepts both a `timedelta` and a plain float number of seconds, as before.
|
||||
- `client.ping()` returns a `bool`.
|
||||
- `client.transport.get_session_id()` returns `None` on protocol eras that have no session, rather than raising. (The SDK v2 removed session-id access from its streamable HTTP transport; FastMCP reconstructs it on the transport object.)
|
||||
|
||||
## What you must change
|
||||
|
||||
Everything above, FastMCP handled for you. What remains lives in your own code, where FastMCP can't reach it — your imports, how you construct errors, the custom HTTP clients you hand to a transport, and any place you reach past FastMCP's surfaces into the raw SDK objects. Each surfaces as a clear failure at import or call time, and each is a mechanical fix.
|
||||
|
||||
**Your own `mcp.types` imports.** FastMCP can re-export types, but it can't rewrite imports in your code. Any `from mcp.types import X` or `import mcp.types` in your server or client fails at import time with:
|
||||
|
||||
```
|
||||
ModuleNotFoundError: No module named 'mcp.types'
|
||||
```
|
||||
|
||||
The raw message gives no hint toward the fix, so if you see it after upgrading, this is why. Switch to `from mcp_types import X`.
|
||||
|
||||
**`McpError` construction.** The v1 pattern of wrapping an `ErrorData` and passing it positionally fails under SDK v2 with:
|
||||
|
||||
```
|
||||
TypeError: MCPError.__init__() missing 1 required positional argument: 'message'
|
||||
```
|
||||
|
||||
Note the message prints the class as `MCPError` (uppercase) even though your code wrote `McpError` — the old name is an alias for the SDK's renamed class. Construct the error with keyword arguments instead:
|
||||
|
||||
```python
|
||||
from fastmcp.exceptions import McpError
|
||||
|
||||
# Before (raises TypeError under SDK v2):
|
||||
# raise McpError(ErrorData(code=-32000, message="Client not supported"))
|
||||
|
||||
# After:
|
||||
raise McpError(code=-32000, message="Client not supported")
|
||||
```
|
||||
|
||||
Catching and `err.error.code` are unchanged — only construction moved.
|
||||
|
||||
**Raw session access sees v2 objects.** If you reach past FastMCP's client and server surfaces into `client.session`, `ctx.session`, or the internals of `ctx.request_context`, you're now holding raw SDK v2 objects with snake_case fields and the v2 method signatures. FastMCP does not wrap these; code that depends on their v1 shape needs updating.
|
||||
|
||||
**FastMCP now uses httpx2 exclusively.** FastMCP has replaced `httpx` with [httpx2](https://pypi.org/project/httpx2/), a next-generation httpx fork, across its entire HTTP stack — client transports and every server-side path (auth providers, the OpenAPI integration, the version check). `httpx` is no longer a FastMCP dependency. If you pass a custom client or factory into a FastMCP client transport — `StreamableHttpTransport(httpx_client_factory=...)`, `SSETransport(httpx_client_factory=...)`, `OAuth(httpx_client_factory=...)`, or a custom `httpx.Auth` as `Client(auth=...)` — those objects must now be httpx2. httpx2 is a drop-in fork with the same public API, so the change is an import swap:
|
||||
|
||||
```python
|
||||
# Before
|
||||
import httpx
|
||||
|
||||
transport = StreamableHttpTransport(
|
||||
"https://example.com/mcp",
|
||||
httpx_client_factory=lambda **kwargs: httpx.AsyncClient(verify=False, **kwargs),
|
||||
)
|
||||
|
||||
# After
|
||||
import httpx2
|
||||
|
||||
transport = StreamableHttpTransport(
|
||||
"https://example.com/mcp",
|
||||
httpx_client_factory=lambda **kwargs: httpx2.AsyncClient(verify=False, **kwargs),
|
||||
)
|
||||
```
|
||||
|
||||
The `client` you pass to `FastMCP.from_openapi(client=...)` (and `OpenAPIProvider(client=...)`) is now type-hinted `httpx2.AsyncClient`. FastMCP does not gate on the type, so an existing `httpx.AsyncClient` keeps working at runtime via duck-typing this release — but switching it to `httpx2.AsyncClient` clears the type hint and is the supported path going forward. HTTP made inside your own tools is entirely yours and is unaffected either way.
|
||||
|
||||
**The subtlest break is exception handlers, and no type checker will catch it.** `httpx` very likely remains installed in your environment (the Anthropic, OpenAI, and Google SDKs all depend on it), so code that catches old-httpx exceptions around FastMCP calls still imports and still type-checks — it just never matches, because FastMCP now raises `httpx2` exceptions. The handler silently becomes dead code:
|
||||
|
||||
```python
|
||||
import httpx # still installed transitively — this import works
|
||||
|
||||
try:
|
||||
result = await client.call_tool("fetch", {"url": url})
|
||||
except httpx.ConnectError: # dead code: FastMCP now raises httpx2.ConnectError
|
||||
return fallback()
|
||||
```
|
||||
|
||||
Grep your codebase for `except httpx.` and move those handlers to `httpx2`. The exception hierarchies match name-for-name, so the fix is an import swap — the hard part is remembering to look. One place you are covered automatically: exceptions raised *inside your tools and resources* (for example, a tool whose own old-httpx call gets a 429) are still mapped to `ToolError`/`ResourceError` by FastMCP's error boundary, which recognizes both libraries' exceptions during the transition.
|
||||
|
||||
Two runtime behaviors shift with httpx2, and because the switch is now wholesale they apply to **all** FastMCP HTTP — including server-auth upstream calls, not just the client path. TLS verification uses the operating system's trust store (via `truststore`, honoring `SSL_CERT_FILE`/`SSL_CERT_DIR`) instead of the bundled certifi CA set, so corporate-CA or certifi-pinned setups may verify differently. And the FastMCP HTTP loggers are renamed from `httpx`/`httpcore.*` to `httpx2`/`httpcore2.*` — update any logging filters that select the HTTP stack by logger name.
|
||||
|
||||
## Removed in FastMCP 4
|
||||
|
||||
Deprecations that warned throughout the 3.x line are removed in 4.0. Unlike the bridged changes above, these fail immediately at the call site — a `ModuleNotFoundError`, `ImportError`, `AttributeError`, or `TypeError` — so nothing degrades silently. Every one has a direct replacement, and the fix is mechanical.
|
||||
|
||||
### Moved imports
|
||||
|
||||
The proxy, OpenAPI, and app integrations moved to their permanent homes, and the internal component classes are no longer re-exported from their old aliases:
|
||||
|
||||
| Removed import | Replacement |
|
||||
| --- | --- |
|
||||
| `fastmcp.server.proxy` | `fastmcp.server.providers.proxy` |
|
||||
| `fastmcp.server.openapi` (and `FastMCPOpenAPI`) | `FastMCP` with an `OpenAPIProvider` from `fastmcp.server.providers.openapi` |
|
||||
| `fastmcp.experimental.server.openapi` | `fastmcp.server.providers.openapi` |
|
||||
| `fastmcp.experimental.utilities.openapi` | `fastmcp.utilities.openapi` |
|
||||
| `fastmcp.server.apps`, `fastmcp.server.app` | `fastmcp.apps` (e.g. `AppConfig`) or `fastmcp` (`FastMCPApp`) |
|
||||
| `FunctionTool` / `ParsedFunction` / `tool` from `fastmcp.tools.tool` | `fastmcp.tools.function_tool` |
|
||||
| `FunctionResource` / `resource` from `fastmcp.resources.resource` | `fastmcp.resources.function_resource` |
|
||||
| `FunctionPrompt` / `prompt` from `fastmcp.prompts.prompt` | `fastmcp.prompts.function_prompt` |
|
||||
|
||||
Two renames in the same family are worth calling out because they have no compatibility alias. The response-caching wrapper models lost a spelling typo — `CachableToolResult`, `CachablePromptResult`, and their siblings became `CacheableToolResult`, `CacheablePromptResult`, etc. — so an import of the old spelling from `fastmcp.server.middleware.caching` raises `ImportError`. And `PromptToolMiddleware` / `ResourceToolMiddleware` are gone in favor of the `PromptsAsTools` / `ResourcesAsTools` transforms from `fastmcp.server.transforms` (the `ToolInjectionMiddleware` base class is retained).
|
||||
|
||||
### Removed server methods and `mount()` keywords
|
||||
|
||||
These `FastMCP` methods and keywords have warned since 3.0 and are now removed:
|
||||
|
||||
| Removed | Replacement |
|
||||
| --- | --- |
|
||||
| `FastMCP.as_proxy(sub)` | `create_proxy(sub)` (from `fastmcp.server`) |
|
||||
| `mcp.import_server(sub)` | `mcp.mount(sub)` |
|
||||
| `mcp.mount(sub, prefix="x")` | `mcp.mount(sub, namespace="x")` |
|
||||
| `mcp.mount(sub, as_proxy=True)` | wrap with `create_proxy(sub)`, then `mount` the proxy |
|
||||
| `mcp.add_tool_transformation(name, cfg)` | `mcp.add_transform(ToolTransform({name: cfg}))` |
|
||||
| `mcp.remove_tool_transformation(name)` | removed (was a no-op); hide tools with `mcp.disable(keys=[...])` |
|
||||
| `mcp.remove_tool(name)` | `mcp.local_provider.remove_tool(name)` |
|
||||
|
||||
Two of these replacements are not exact behavioral swaps. `create_proxy` takes its target as the first positional argument (`target`), so a keyword call like `as_proxy(backend=server)` becomes `create_proxy(server)` rather than reusing the old keyword. And `local_provider.remove_tool` raises a plain `KeyError` when the tool is missing, where `FastMCP.remove_tool` raised a `NotFoundError` — update any `except NotFoundError` cleanup around a removal.
|
||||
|
||||
`mount(as_proxy=True)` used to route the child through a proxy (an MCP-client execution boundary) rather than composing it directly. To keep that boundary, wrap the child in `create_proxy()` and mount the proxy; a plain `mount(child)` composes the child in-process. Either way, the child's lifespan and middleware now run — a direct mount no longer skips them.
|
||||
|
||||
`import_server` → `mount` is the one row here that is not a mechanical swap, because the two never had the same semantics. `import_server` took a **one-time static snapshot** — it copied the child's tools, resources, and prompts at call time, with no live link, and did not run the child's lifespan or middleware. `mount` is a **live composition** — it holds a live link to the child and runs the child's lifespan and middleware. After switching, later changes to the child become visible through the parent, the child's lifespan runs with the parent's (entered when the server starts, held until it stops — not per request), and the child's middleware runs on the operations delegated to it. If you depended on the frozen-copy behavior (a stable snapshot, no child lifecycle), there is no drop-in replacement: register the child's components on the parent directly instead of composing the two servers.
|
||||
|
||||
### Removed tool and decorator parameters
|
||||
|
||||
Two `@tool` parameters and two settings are gone:
|
||||
|
||||
- **Tool `serializer=`** is removed from `@tool` / `mcp.tool()`, `Tool.from_function`, `Tool.from_tool`, and the OpenAPI tool. Return a `ToolResult` from your tool for full control over serialization instead.
|
||||
- **Tool `exclude_args=`** is removed. Hide a parameter from the tool schema by injecting it instead: give it a `Depends(factory)` default (from `fastmcp.dependencies`), where `factory` is a callable returning the value the argument used to carry. An injected parameter never appears in the tool's schema, which is what `exclude_args` was for.
|
||||
- **The `decorator_mode` setting** (`FASTMCP_DECORATOR_MODE`) and its `"object"` mode are removed. Decorators always return your original function with metadata attached; reach the component object through the server (`await mcp.get_tool("name")`) rather than off the decorated function.
|
||||
- **`StreamableHttpTransport(sse_read_timeout=...)`** is removed — it was a no-op under the SDK v2 client. Set the read timeout through the public `Client(transport, timeout=...)` (a `timedelta` or float seconds), or reach for a custom `httpx_client_factory` when you need finer control. (`SSETransport` still accepts `sse_read_timeout`.)
|
||||
|
||||
## Behavior changes to verify
|
||||
|
||||
Two server-side behaviors changed in ways that compile fine but can surface at runtime.
|
||||
|
||||
**Templated resources are path-screened by default.** Every templated resource now has its extracted parameter values checked for path-traversal (`..` segments), absolute paths, and null bytes *before your handler runs*, at the server's read chokepoint. A rejected read returns a non-leaky "resource not found" error. Only a standalone `..` segment counts as traversal, so values that merely contain dots (`file.tar.gz`, `HEAD~3..HEAD`) and dotfiles (`.env`) still pass. If a template legitimately accepts `..`-bearing or absolute values, exempt the parameter with `ResourceSecurity(exempt_params={...})`, disable the check per-component with `security=None`, or set a server-wide default with `FastMCP(resource_security=...)`. See [Resources → Path Security](/servers/resources#path-security).
|
||||
|
||||
**Resource-not-found now returns `-32602`.** The wire error code for a missing resource from the core `resources/read` handler changed from `-32002` to `-32602` (`INVALID_PARAMS`, per SEP-2164). The human-readable message ("Resource not found: ...") is unchanged, so this only affects clients that matched on the numeric code — update those to expect `-32602`. (The opt-in `ErrorHandlingMiddleware` keeps its own per-method-prefix code mapping; if you run it with `transform_errors=True` it can still map not-found to a different code, so it is unaffected by this change.)
|
||||
|
||||
## Deprecation timeline
|
||||
|
||||
The camelCase bridge is a migration aid, not a permanent fixture. It works today and warns on every bridged read so you can find and update the affected call sites. Plan to migrate your reads to snake_case: the shims will be removed in a future release, after which only the snake_case names resolve — the same state you get today by setting `mcp_camelcase_compat = False`. Turning the setting off is a good way to surface every remaining camelCase read in your code as a hard `AttributeError` before the shims go away.
|
||||
|
||||
## SDK deprecation warnings you may see
|
||||
|
||||
Ordinary use of `ctx.info` (client logging) and `ctx.sample` now emits an SDK-level `MCPDeprecationWarning`:
|
||||
|
||||
```
|
||||
The logging/sampling capability is deprecated as of 2026-07-28 (SEP-2577)
|
||||
```
|
||||
|
||||
These warnings come from the MCP SDK, not from FastMCP. For logging they are benign: `ctx.info` keeps working on session-based connections exactly as the protocol table below describes, and the SDK is only signaling the protocol's direction. For sampling, FastMCP additionally emits its own `FastMCPDeprecationWarning`: `ctx.sample` and `ctx.sample_step` are deprecated and slated for removal, so treat that warning as a prompt to migrate to server-side LLM calls rather than as informational.
|
||||
|
||||
## Protocol version support
|
||||
|
||||
FastMCP servers built on the SDK v2 serve multiple protocol eras from the same server. The SDK negotiates the era each client speaks: the sessionless `2026-07-28` era (which discovers capabilities through `server/discover`) and earlier session-based handshake versions are all handled simultaneously. This formally supersedes FastMCP's earlier "latest protocol only" stance — a single server now works with clients across the protocol transition.
|
||||
|
||||
Not every Context feature is available on every era yet. The imperative push APIs that call back into the client mid-execution — `ctx.elicit`, `ctx.sample`, and `ctx.list_roots` — depend on the session-based back-channel of the earlier eras, so on a `2026-07-28` connection they raise a clear, era-aware error rather than reaching the client. Elicitation itself still reaches the user on the modern era, through the guard pattern: a tool *returns* an `InputRequiredResult` describing what it needs, and the client answers with a fresh call (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)). Logging notifications and the request/response features flow on every era.
|
||||
|
||||
Sampling is the exception that does not come back, and the reason is the protocol rather than an unfinished FastMCP feature. SEP-2577 deprecated server-initiated sampling, so `ctx.sample` and `ctx.sample_step` are **deprecated** and will be removed in a future FastMCP release. Elicitation moved to the guard pattern because the modern protocol still carries elicitation requests; sampling has no equivalent path because the protocol deprecated the pattern itself. The migration is to call an LLM directly from your server rather than borrowing the client's model. See [Sampling](/servers/sampling) for details.
|
||||
|
||||
| Context feature | Earlier eras (session-based) | `2026-07-28` (sessionless) |
|
||||
| --- | --- | --- |
|
||||
| `ctx.info` / logging notifications | Supported | Supported |
|
||||
| Tools, resources, prompts, completions | Supported | Supported |
|
||||
| `ctx.elicit` | Supported | Use the guard pattern (return `InputRequiredResult`) |
|
||||
| `ctx.sample` / `ctx.sample_step` | Supported (deprecated) | Removed — call an LLM server-side |
|
||||
| `ctx.list_roots` | Supported | Via the guard pattern (`input_requests` carries roots requests) |
|
||||
| `Middleware.on_initialize` | Runs on connect | Never runs — there is no `initialize` handshake |
|
||||
| Session state (`ctx.set_state` across calls) | Persists for the session | Does not persist — every request is a fresh connection |
|
||||
| Background tasks (`task=True`) | Runs synchronously — never tasked | Supported via the tasks extension |
|
||||
|
||||
If your tools rely on `ctx.elicit` or `ctx.list_roots`, they continue to work against clients on the earlier eras; on the modern era, reach for the guard pattern instead (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)). Sampling is deprecated on every era and will not return on modern connections — migrate those tools to server-side LLM calls.
|
||||
|
||||
Two of these bite by default now, because **`fastmcp.Client` defaults to `mode="auto"`** in v4 — an ordinary `Client(server)` negotiates the newest protocol both sides share, which against a FastMCP server is the sessionless `2026-07-28` era. On that era there is no `initialize` handshake, so a `Middleware.on_initialize` hook never runs; and each request is a fresh connection, so state written with `ctx.set_state` in one call is not visible in the next. A server that gates access in `on_initialize` or relies on per-session state must keep its clients on the session-based era. The narrow escape is per-client: `Client(server, mode="legacy")`. The durable, server-side answer is to declare the versions the server actually serves so a modern client is refused at connect time rather than silently losing those features — see the server's protocol-version restriction (added alongside this change).
|
||||
|
||||
## Upgrade checklist
|
||||
|
||||
Most servers upgrade untouched. Work down this list to find the ones that don't:
|
||||
|
||||
1. **Bump your environment.** Raise any pin below `pydantic>=2.12`; upgrade FastAPI if your resolver complains about Starlette `<1.0.1`.
|
||||
2. **Fix imports that moved out.** Replace `from mcp.types import X` with `from mcp_types import X`, and update any import from the [removed modules](#moved-imports) (`fastmcp.server.proxy`, `fastmcp.server.openapi`, `fastmcp.server.apps`, the `fastmcp.tools.tool` / `resources.resource` / `prompts.prompt` component shims).
|
||||
3. **Update removed server APIs.** Swap `as_proxy` → `create_proxy`, `import_server` → `mount`, `mount(prefix=)` → `mount(namespace=)`, and the [other removed methods and keywords](#removed-server-methods-and-mount-keywords).
|
||||
4. **Update removed tool parameters.** Replace tool `serializer=` (return a `ToolResult`), `exclude_args=` (use `Depends()`), and `StreamableHttpTransport(sse_read_timeout=)`.
|
||||
5. **Fix `McpError` construction.** Positional `McpError(ErrorData(...))` becomes keyword `McpError(code=..., message=...)`. Catching is unchanged.
|
||||
6. **Move httpx to httpx2.** Grep for `except httpx.` and for custom `httpx_client_factory` / `httpx.Auth` objects handed to FastMCP, and swap the import to `httpx2`.
|
||||
7. **Decide the client era.** `Client` now defaults to `mode="auto"`. If a server relies on `on_initialize` or per-session state, keep its clients on `mode="legacy"` or restrict the server's served protocol versions.
|
||||
8. **Verify behavior changes.** Confirm templated resources that legitimately accept `..` or absolute paths are exempted, and update any client that matched the old `-32002` resource-not-found code.
|
||||
9. **Run with the camelCase bridge off.** Set `mcp_camelcase_compat = False` (or `FASTMCP_MCP_CAMELCASE_COMPAT=false`) in CI to surface every remaining camelCase read as a hard `AttributeError` before the shims are removed.
|
||||
|
||||
The executable version of this checklist lives in [`tests/test_upgrade_from_v3.py`](https://github.com/PrefectHQ/fastmcp/blob/main/tests/test_upgrade_from_v3.py): it builds representative 3.x-style servers and asserts they run unchanged, and pins every removed surface to the exact error it now raises.
|
||||
|
|
@ -9,16 +9,18 @@ If you've been building MCP servers directly on the `mcp` package's `Server` cla
|
|||
|
||||
The core idea: instead of telling the SDK what your tools look like and then separately implementing them, you write ordinary Python functions and let FastMCP derive the protocol layer from your code. Type hints become JSON Schema. Docstrings become descriptions. Return values are serialized automatically. The plumbing you wrote to satisfy the protocol just disappears.
|
||||
|
||||
<Note>
|
||||
This guide covers upgrading from **v1** of the `mcp` package. We'll provide a separate guide when v2 ships.
|
||||
</Note>
|
||||
## Why now is the moment to switch
|
||||
|
||||
MCP SDK v2 landed sweeping breaking changes on the low-level `Server`: the protocol types moved out of `mcp.types` into a separate `mcp_types` package, every field was renamed from camelCase to snake_case, the `Server` class was rebuilt, `McpError` was renamed, and sessions were removed on the new sessionless protocol era. If you build directly on the low-level SDK, all of that lands on you — you have to rewrite your imports, your handler signatures, and your error construction to match the new surface.
|
||||
|
||||
Adopting FastMCP is the easier path. FastMCP 4 runs on SDK v2 and hides that entire surface behind a high-level API that did not change. You write `@mcp.tool` and never touch the renamed internals — FastMCP derives the protocol layer from your function signatures, so the SDK v2 rename simply isn't something your code has to know about. Migrating low-level-SDK-v1 code to FastMCP is less work than migrating it to raw SDK v2, and you come out the other side with the whole framework: composition, middleware, proxies, authentication, and testing. The SDK v2 break is the natural moment to make the jump.
|
||||
|
||||
<Note>
|
||||
Already using FastMCP 1.0 via `from mcp.server.fastmcp import FastMCP`? Your upgrade is simpler — see the [FastMCP 1.0 upgrade guide](/getting-started/upgrading/from-mcp-sdk) instead.
|
||||
</Note>
|
||||
|
||||
<Prompt description="Copy this prompt into any LLM along with your server code to get automated upgrade guidance.">
|
||||
You are upgrading an MCP server from the `mcp` package's low-level Server class (v1) to FastMCP 3.0. The server currently uses `mcp.server.Server` (or `mcp.server.lowlevel.server.Server`) with manual handler registration. Analyze the provided code and rewrite it using FastMCP's high-level API. The full guide is at https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk and the complete FastMCP documentation is at https://gofastmcp.com — fetch these for complete context.
|
||||
You are upgrading an MCP server from the `mcp` package's low-level Server class (v1) to FastMCP 4. The server currently uses `mcp.server.Server` (or `mcp.server.lowlevel.server.Server`) with manual handler registration. Analyze the provided code and rewrite it using FastMCP's high-level API. The full guide is at https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk and the complete FastMCP documentation is at https://gofastmcp.com — fetch these for complete context.
|
||||
|
||||
UPGRADE RULES:
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ uv add fastmcp
|
|||
FastMCP includes the `mcp` package as a dependency, so you don't lose access to anything. Update your import, run your server, and if your tools work, you're done.
|
||||
|
||||
<Prompt description="Copy this prompt into any LLM along with your server code to get automated upgrade guidance.">
|
||||
You are upgrading an MCP server from FastMCP 1.0 (bundled in the `mcp` package v1) to standalone FastMCP 3.0. Analyze the provided code and identify every change needed. The full upgrade guide is at https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk and the complete FastMCP documentation is at https://gofastmcp.com — fetch these for complete context.
|
||||
You are upgrading an MCP server from FastMCP 1.0 (bundled in the `mcp` package v1) to standalone FastMCP 4. Analyze the provided code and identify every change needed. The full upgrade guide is at https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk and the complete FastMCP documentation is at https://gofastmcp.com — fetch these for complete context.
|
||||
|
||||
STEP 1 — IMPORT (required for all servers):
|
||||
Change "from mcp.server.fastmcp import FastMCP" to "from fastmcp import FastMCP".
|
||||
|
|
@ -51,9 +51,9 @@ Also: if prompts return raw dicts like `{"role": "user", "content": "..."}`, the
|
|||
The MCP SDK's FastMCP 1.0 silently coerced dicts; standalone FastMCP requires typed returns.
|
||||
|
||||
STEP 4 — OTHER MCP IMPORTS (only if importing from mcp.* directly):
|
||||
Direct imports from the `mcp` package (e.g., `import mcp.types`, `from mcp.server.stdio import stdio_server`) still work because FastMCP includes `mcp` as a dependency. However, prefer FastMCP's own APIs where equivalents exist:
|
||||
- mcp.types.TextContent for tool returns → just return plain Python values (str, int, dict, etc.)
|
||||
- mcp.types.ImageContent → fastmcp.utilities.types.Image
|
||||
FastMCP now builds on MCP SDK v2, which removed the `mcp.types` module — protocol types live in the standalone `mcp_types` package. Update any `from mcp.types import X` to `from mcp_types import X`. Prefer FastMCP's own APIs where equivalents exist:
|
||||
- mcp_types.TextContent for tool returns → just return plain Python values (str, int, dict, etc.)
|
||||
- mcp_types.ImageContent → fastmcp.utilities.types.Image
|
||||
- from mcp.server.stdio import stdio_server → not needed, mcp.run() handles transport
|
||||
|
||||
STEP 5 — DECORATORS (only if treating decorated functions as objects):
|
||||
|
|
@ -113,7 +113,7 @@ def debug(error: str) -> list[Message]:
|
|||
|
||||
### Other `mcp.*` Imports
|
||||
|
||||
If your server imports directly from the `mcp` package — like `import mcp.types` or `from mcp.server.stdio import stdio_server` — those still work. FastMCP includes `mcp` as a dependency, so nothing breaks.
|
||||
FastMCP now builds on MCP SDK v2. The `mcp.types` module no longer exists — protocol types moved to a standalone `mcp_types` package, and the field names were renamed from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, and so on). Update `from mcp.types import X` to `from mcp_types import X`. For the full picture, see [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3).
|
||||
|
||||
Where FastMCP provides its own API for the same thing, it's worth switching over:
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ Where FastMCP provides its own API for the same thing, it's worth switching over
|
|||
| `mcp.types.PromptMessage(...)` | `from fastmcp.prompts import Message` |
|
||||
| `from mcp.server.stdio import stdio_server` | Not needed — `mcp.run()` handles transport |
|
||||
|
||||
For anything without a FastMCP equivalent (e.g., specific protocol types you use directly), the `mcp.*` import is fine to keep.
|
||||
For protocol types without a FastMCP equivalent, import them from `mcp_types` directly.
|
||||
|
||||
### Decorated Functions
|
||||
|
||||
|
|
|
|||
99
docs/getting-started/whats-new.mdx
Normal file
99
docs/getting-started/whats-new.mdx
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
---
|
||||
title: "What's New in FastMCP 4"
|
||||
sidebarTitle: "What's New"
|
||||
description: The capabilities that define FastMCP 4 — a rebuilt engine, a new protocol era, and a stateless protocol made practical.
|
||||
icon: sparkles
|
||||
---
|
||||
|
||||
FastMCP 4 is a major version because its engine changed. The framework is now built on the MCP Python SDK v2, a ground-up rebuild of the protocol layer, and on that foundation it adds a new protocol era, first-class extensions, stateless state, enterprise identity, and more. Most FastMCP 3 servers run on it untouched — the major version signals how much moved underneath, and what that movement unlocks.
|
||||
|
||||
<Note>
|
||||
FastMCP 4 is in **alpha**. Pin an exact version and expect sharp edges.
|
||||
</Note>
|
||||
|
||||
## Built on the MCP Python SDK v2
|
||||
|
||||
The defining change in FastMCP 4 is the one you mostly can't see. The MCP Python SDK v2 rewrote the protocol layer end to end: it split the protocol types into a standalone `mcp_types` package, renamed every wire field from camelCase to snake_case, replaced the server's request-handling model, and made server-side middleware and multi-era serving first-class. FastMCP absorbs nearly all of it — your reads stay working through a compatibility bridge, and the handful of changes left in your code are mechanical.
|
||||
|
||||
The major version is the signal. Even where your surface is unchanged, the behavior underneath is substantially different, and bumping to 4.0 is how we tell you that plainly rather than slipping a new engine in under a patch release.
|
||||
|
||||
The rebuild also pulls the protocol's recent evolution forward in a single step. A batch of accepted MCP proposals arrives with SDK v2, and FastMCP 4 surfaces each one: capability-negotiated extensions (SEP-2133), multi-round-trip elicitation for sessionless connections (SEP-2322), response cache hints (SEP-2549), spec-standard error codes (SEP-2164), the enterprise identity-assertion grant (SEP-990), and the sessionless `2026-07-28` protocol itself, which removes server-initiated requests (SEP-2577). The rest of this page is what those add up to.
|
||||
|
||||
## Every protocol era
|
||||
|
||||
A FastMCP 4 server answers clients across the protocol transition from one deployment. The MCP SDK negotiates the era per connection — the sessionless `2026-07-28` protocol for clients that have moved forward, the session-based handshake for everyone else — and any replica behind a plain load balancer can serve a modern request. This supersedes FastMCP's earlier "latest protocol only" stance: you adopt the new protocol without forking your deployment or gating clients by version.
|
||||
|
||||
The same negotiation runs from the client, and its default flipped. A plain `Client(url)` now probes for the modern protocol and adopts it when the server offers it, falling back to the handshake otherwise — where every earlier FastMCP version pinned the handshake outright. That flip is what brings the modern capabilities within reach of ordinary client code: a task-enabled tool hands back a handle to poll, and multi-round-trip elicitation resolves across successive requests, neither requiring the caller to opt in. Set `mode="legacy"` to pin the handshake when you need the session-based back-channel or the classic `initialize` result. See [Protocol negotiation](/clients/client#protocol-negotiation).
|
||||
|
||||
The modern protocol is sessionless, so it drops the server's ability to call back into the client mid-request (SEP-2577). Imperative `ctx.elicit` and `ctx.list_roots` move to a request-shaped pattern on modern connections, and server-initiated sampling — which has no such replacement — is [deprecated](/servers/sampling). Everything else about writing a server is unchanged.
|
||||
|
||||
## State without a session
|
||||
|
||||
A stateless protocol raises an obvious question: if every request is a fresh connection, where does a tool keep a shopping cart, a conversation, or a running total? FastMCP 4 follows the MCP working group's own decision to reject protocol-level sessions in favor of *explicit state handles* (SEP-2567) — the server hands out an identifier, and the client passes it back.
|
||||
|
||||
Two shapes cover the cases. `UserSession` is injected like `Context` and keyed to the authenticated user, so a tool reads and writes one bucket of state with nothing to pass around. `SessionId` is an explicit handle a tool mints and the caller supplies as an argument, for when one user holds many independent states. Both store their data server-side in the storage backend, keyed to the authenticated user — so a handle is inert in anyone else's hands. See [Session State](/servers/sessions).
|
||||
|
||||
## Background tasks
|
||||
|
||||
Long-running work runs as a background task: the server accepts the call, returns a handle, and the client polls for the result while the work proceeds. Tasks left the core MCP spec during the SDK v2 rebuild and returned as the `io.modelcontextprotocol/tasks` extension (SEP-2663), which FastMCP implements end to end in the optional `fastmcp-tasks` package. The durable execution engine that made FastMCP 3's tasks reliable — [Docket](https://github.com/chrisguidry/docket) — carries straight over, and `@mcp.tool(task=True)` remains the authoring surface, so the wire protocol modernizing underneath costs you no code change. See [Background Tasks](/servers/tasks).
|
||||
|
||||
## Server extensions
|
||||
|
||||
Background tasks are the first capability built on a more general one: FastMCP 4 makes MCP extensions — capability-negotiated protocol features named by a reverse-DNS string (SEP-2133) — a first-class surface. `FastMCP.add_extension()` lets an extension advertise a capability, add request methods, intercept `tools/call`, and run a lifespan hook, all with full access to the component registry, `Context`, and auth. The same extensions flow through the client with `Client(extensions=...)`. A cross-cutting protocol feature stops being surgery on core and becomes a supported plugin.
|
||||
|
||||
## Argument completion
|
||||
|
||||
When a client offers autocomplete for a prompt argument or a resource-template parameter, it asks the server which values fit — narrowing the list as the user types. FastMCP 4 lets a server answer. A single `@mcp.completion` handler receives the reference being completed, the argument and its partial value, and the arguments the user has already supplied, and returns the candidates the client surfaces as suggestions. Because the handler sees the earlier arguments, completions can depend on them — a `repo` parameter suggesting only repositories under the `owner` already chosen.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from mcp_types import PromptReference
|
||||
|
||||
mcp = FastMCP("Docs")
|
||||
|
||||
|
||||
@mcp.prompt
|
||||
def write_poem(theme: str) -> str:
|
||||
return f"Write a poem about {theme}"
|
||||
|
||||
|
||||
@mcp.completion
|
||||
def complete(ref, argument, context):
|
||||
if isinstance(ref, PromptReference) and argument.name == "theme":
|
||||
options = ["nature", "love", "adventure"]
|
||||
return [o for o in options if o.startswith(argument.value)]
|
||||
return None
|
||||
```
|
||||
|
||||
Registering a handler advertises the completions capability during negotiation, so a client only sends requests to a server that answers them — the same on both protocol eras. See [Argument Completion](/servers/completions).
|
||||
|
||||
## Enterprise identity
|
||||
|
||||
FastMCP 4 ships a complete server-side implementation of identity assertion (SEP-990): enterprise "on-behalf-of" access, where a corporate identity provider issues a signed assertion, the user's agent presents it, and the server mints a short-lived token — no browser login and no per-user consent screen. Behind one parameter on the existing auth providers, FastMCP performs the full signature verification, binding checks, replay rejection, and scoped token issuance.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth import IdentityAssertion, OAuthProxy
|
||||
|
||||
auth = OAuthProxy(
|
||||
# existing upstream configuration unchanged
|
||||
identity_assertion=IdentityAssertion(trusted_issuers=["https://login.acme-corp.com"]),
|
||||
)
|
||||
mcp = FastMCP("Internal API", auth=auth)
|
||||
```
|
||||
|
||||
The asserted subject flows into the normal auth context, so tools read it through `get_access_token()` like any other identity. See [Identity Assertion](/servers/auth/oauth-proxy#identity-assertion-sep-990).
|
||||
|
||||
## Faster and safer
|
||||
|
||||
Two more capabilities arrive by default. Response caching (SEP-2549) lets a server stamp freshness hints on its results that a caching [client](/clients/client#response-caching) reuses without a round trip, and a distributed `KeyValueResponseCacheStore` backs that cache with Redis or any key-value store, so a fleet of clients or proxy replicas shares fills.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Weather", cache_ttl=300, cache_scope="public")
|
||||
```
|
||||
|
||||
Security tightened in the same release: every templated resource screens its parameters for path traversal, absolute paths, and null bytes before the handler runs — [path security](/servers/resources#path-security) on by default, covering mounted and proxied templates too.
|
||||
|
||||
When you're ready to move a server to v4, [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) walks through every change and what it looks like in practice.
|
||||
|
|
@ -9,9 +9,54 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
|
|||
|
||||
<VersionBadge version="2.12.4" />
|
||||
|
||||
This guide shows you how to secure your FastMCP server using **Auth0 OAuth**. While Auth0 does have support for Dynamic Client Registration, it is not enabled by default so this integration uses the [**OIDC Proxy**](/servers/auth/oidc-proxy) pattern to bridge Auth0's dynamic OIDC configuration with MCP's authentication requirements.
|
||||
FastMCP supports two Auth0 integration paths:
|
||||
|
||||
## Configuration
|
||||
- **[Auth for MCP](#auth-for-mcp-dcr)** — Auth0 handles OAuth, DCR, and CIMD; FastMCP validates tokens (`Auth0MCPProvider`). Use this for MCP-native clients and Auth0's [Auth for MCP](https://auth0.com/ai/docs/mcp/intro/overview) setup.
|
||||
- **[OIDC Proxy](#oidc-proxy-fixed-credentials)** — FastMCP proxies OAuth with fixed application credentials (`Auth0Provider`). Use this when you manage an Auth0 application manually and do not need tenant-level DCR.
|
||||
|
||||
## Auth for MCP (DCR)
|
||||
|
||||
<VersionBadge version="3.3.0" />
|
||||
|
||||
This path uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern. Auth0 acts as the authorization server; FastMCP is the resource server.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. An **[Auth0 account](https://auth0.com/)** with **Auth for MCP** enabled
|
||||
2. **Resource Parameter Compatibility Profile** enabled (Settings → Advanced)
|
||||
3. Your FastMCP server URL (use `http://127.0.0.1:8000` in development — not `localhost`)
|
||||
|
||||
See Auth0's [authorization quickstart](https://auth0.com/ai/docs/mcp/get-started/authorization-for-your-mcp-server) for tenant setup (API identifier, domain-level connections, CIMD approval).
|
||||
|
||||
### Step 1: Create an Auth0 API
|
||||
|
||||
Create an API (Resource Server) whose **identifier** is your MCP resource URL, for example `http://127.0.0.1:8000/mcp`. Use `RS256` signing and the `rfc9068_profile_authz` token dialect if you need `permissions` claims on tokens.
|
||||
|
||||
When the server starts, it logs the exact `aud` value it validates — your API identifier must match.
|
||||
|
||||
### Step 2: FastMCP configuration
|
||||
|
||||
```python server_mcp.py
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.auth0 import Auth0MCPProvider
|
||||
|
||||
auth_provider = Auth0MCPProvider(
|
||||
config_url="https://YOUR_TENANT.auth0.com/.well-known/openid-configuration",
|
||||
base_url="http://127.0.0.1:8000",
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Auth0 MCP Server", auth=auth_provider)
|
||||
```
|
||||
|
||||
No `client_id` or `client_secret` is required on the FastMCP side — MCP clients register with Auth0 directly.
|
||||
|
||||
### Testing
|
||||
|
||||
See `examples/auth/auth0_mcp/` for a runnable server and DCR client. Set `AUTH0_CONFIG_URL` to your tenant's OIDC discovery URL before starting the server.
|
||||
|
||||
## OIDC Proxy (fixed credentials)
|
||||
|
||||
This integration uses the [**OIDC Proxy**](/servers/auth/oidc-proxy) pattern when you use a fixed Auth0 application instead of tenant-level DCR.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
|
|
@ -137,7 +182,8 @@ async def main():
|
|||
|
||||
# Test the protected tool
|
||||
result = await client.call_tool("get_token_info")
|
||||
print(f"Auth0 audience: {result['audience']}")
|
||||
token_info = result.data
|
||||
print(f"Auth0 audience: {token_info['audience']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ auth = OAuth(additional_client_metadata={"token_endpoint_auth_method": "none"})
|
|||
|
||||
async def main():
|
||||
async with Client("http://127.0.0.1:8000/mcp", auth=auth) as client:
|
||||
assert await client.ping()
|
||||
tools = await client.list_tools()
|
||||
print(f"Authenticated. Server exposes {len(tools)} tools.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
|
|||
|
|
@ -223,8 +223,9 @@ async def main():
|
|||
|
||||
# Test the protected tool
|
||||
result = await client.call_tool("get_user_info")
|
||||
print(f"Azure user: {result['email']}")
|
||||
print(f"Name: {result['name']}")
|
||||
user_info = result.data
|
||||
print(f"Azure user: {user_info['email']}")
|
||||
print(f"Name: {user_info['name']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -409,7 +410,7 @@ The `EntraOBOToken` dependency handles the complete OBO flow automatically. Decl
|
|||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.azure import AzureProvider, EntraOBOToken
|
||||
import httpx
|
||||
import httpx2
|
||||
|
||||
auth_provider = AzureProvider(
|
||||
client_id="your-client-id",
|
||||
|
|
@ -431,7 +432,7 @@ async def get_recent_emails(
|
|||
graph_token: str = EntraOBOToken(["https://graph.microsoft.com/Mail.Read"]),
|
||||
) -> list[dict]:
|
||||
"""Get the user's recent emails from Microsoft Graph."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx2.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"https://graph.microsoft.com/v1.0/me/messages?$top={count}",
|
||||
headers={"Authorization": f"Bearer {graph_token}"},
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ The connector must be explicitly enabled in each chat session through Developer
|
|||
Use `annotations=ToolAnnotations(readOnlyHint=True)` to skip confirmation prompts for read-only tools:
|
||||
|
||||
```python
|
||||
from mcp.types import ToolAnnotations
|
||||
from mcp_types import ToolAnnotations
|
||||
|
||||
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
||||
def get_status() -> str:
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ fastmcp install claude-code server.py --project /path/to/my-project
|
|||
If your server needs environment variables (like API keys), you must include them:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-code server.py --server-name "Weather Server" \
|
||||
fastmcp install claude-code server.py --name "Weather Server" \
|
||||
--env API_KEY=your-api-key \
|
||||
--env DEBUG=true
|
||||
```
|
||||
|
|
@ -126,7 +126,7 @@ fastmcp install claude-code server.py --server-name "Weather Server" \
|
|||
Or load them from a `.env` file:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-code server.py --server-name "Weather Server" --env-file .env
|
||||
fastmcp install claude-code server.py --name "Weather Server" --env-file .env
|
||||
```
|
||||
|
||||
<Warning>
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ Claude Desktop runs servers in a completely isolated environment with no access
|
|||
If your server needs environment variables (like API keys), you must include them:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-desktop server.py --server-name "Weather Server" \
|
||||
fastmcp install claude-desktop server.py --name "Weather Server" \
|
||||
--env API_KEY=your-api-key \
|
||||
--env DEBUG=true
|
||||
```
|
||||
|
|
@ -149,7 +149,7 @@ fastmcp install claude-desktop server.py --server-name "Weather Server" \
|
|||
Or load them from a `.env` file:
|
||||
|
||||
```bash
|
||||
fastmcp install claude-desktop server.py --server-name "Weather Server" --env-file .env
|
||||
fastmcp install claude-desktop server.py --name "Weather Server" --env-file .env
|
||||
```
|
||||
<Warning>
|
||||
- **`uv` must be installed and available in your system PATH**. Claude Desktop runs in its own isolated environment and needs `uv` to manage dependencies.
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ Cursor runs servers in a completely isolated environment with no access to your
|
|||
If your server needs environment variables (like API keys), you must include them:
|
||||
|
||||
```bash
|
||||
fastmcp install cursor server.py --server-name "Weather Server" \
|
||||
fastmcp install cursor server.py --name "Weather Server" \
|
||||
--env API_KEY=your-api-key \
|
||||
--env DEBUG=true
|
||||
```
|
||||
|
|
@ -147,7 +147,7 @@ fastmcp install cursor server.py --server-name "Weather Server" \
|
|||
Or load them from a `.env` file:
|
||||
|
||||
```bash
|
||||
fastmcp install cursor server.py --server-name "Weather Server" --env-file .env
|
||||
fastmcp install cursor server.py --name "Weather Server" --env-file .env
|
||||
```
|
||||
|
||||
<Warning>
|
||||
|
|
@ -164,10 +164,10 @@ You can generate MCP JSON configuration for manual use:
|
|||
|
||||
```bash
|
||||
# Generate configuration and output to stdout
|
||||
fastmcp install mcp-json server.py --server-name "Dice Roller" --with pandas
|
||||
fastmcp install mcp-json server.py --name "Dice Roller" --with pandas
|
||||
|
||||
# Copy configuration to clipboard for easy pasting
|
||||
fastmcp install mcp-json server.py --server-name "Dice Roller" --copy
|
||||
fastmcp install mcp-json server.py --name "Dice Roller" --copy
|
||||
```
|
||||
|
||||
This generates the standard `mcpServers` configuration format that can be used with any MCP-compatible client.
|
||||
|
|
|
|||
|
|
@ -18,16 +18,15 @@ This guide shows you how to secure your FastMCP server using [**Descope**](https
|
|||
Before you begin, you will need:
|
||||
|
||||
1. To [sign up](https://www.descope.com/sign-up) for a Free Forever Descope account
|
||||
2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:3000`)
|
||||
2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
|
||||
|
||||
### Step 1: Configure Descope
|
||||
|
||||
<Steps>
|
||||
<Step title="Create an MCP Server">
|
||||
1. Go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console, and create a new MCP Server.
|
||||
2. Give the MCP server a name and description.
|
||||
3. Ensure that **Dynamic Client Registration (DCR)** is enabled. Then click **Create**.
|
||||
4. Once you've created the MCP Server, note your Well-Known URL.
|
||||
<Step title="Configure a Descope application">
|
||||
You can use either a resource-specific Descope MCP Server or a project-level inbound app.
|
||||
|
||||
To create an MCP Server, go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console, create a server, and enable **Dynamic Client Registration (DCR)**.
|
||||
|
||||
|
||||
<Warning>
|
||||
|
|
@ -35,10 +34,17 @@ Before you begin, you will need:
|
|||
</Warning>
|
||||
</Step>
|
||||
|
||||
<Step title="Note Your Well-Known URL">
|
||||
Save your Well-Known URL from [MCP Server Settings](https://app.descope.com/mcp-servers):
|
||||
<Step title="Copy the Well-Known URL">
|
||||
`DescopeProvider` accepts both resource-specific MCP Server URLs:
|
||||
|
||||
```
|
||||
Well-Known URL: https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration
|
||||
https://api.descope.com/v1/apps/agentic/P.../M.../.well-known/openid-configuration
|
||||
```
|
||||
|
||||
and project-level inbound app URLs:
|
||||
|
||||
```
|
||||
https://api.descope.com/v1/apps/P.../.well-known/openid-configuration
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
|
@ -48,30 +54,52 @@ Before you begin, you will need:
|
|||
Create a `.env` file with your Descope configuration:
|
||||
|
||||
```bash
|
||||
DESCOPE_CONFIG_URL=https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration # Your Descope Well-Known URL
|
||||
SERVER_URL=http://localhost:3000 # Your server's base URL
|
||||
DESCOPE_CONFIG_URL=https://api.descope.com/v1/apps/P.../.well-known/openid-configuration
|
||||
BASE_URL=http://localhost:8000
|
||||
```
|
||||
|
||||
### Step 3: FastMCP Configuration
|
||||
|
||||
Create your FastMCP server file and use the DescopeProvider to handle all the OAuth integration automatically:
|
||||
Create your FastMCP server file and use the DescopeProvider to handle all the OAuth integration automatically. Nothing reads `.env` automatically, so load it explicitly with [python-dotenv](https://pypi.org/project/python-dotenv/) (`pip install python-dotenv`) before constructing the provider — otherwise the values you just wrote stay invisible to `os.environ`.
|
||||
|
||||
```python server.py
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.descope import DescopeProvider
|
||||
|
||||
# The DescopeProvider automatically discovers Descope endpoints
|
||||
# and configures JWT token validation
|
||||
load_dotenv()
|
||||
|
||||
# DescopeProvider accepts either supported Well-Known URL format.
|
||||
auth_provider = DescopeProvider(
|
||||
config_url="https://.../.well-known/openid-configuration", # Your MCP Server .well-known URL
|
||||
base_url=SERVER_URL, # Your server's public URL
|
||||
config_url=os.environ["DESCOPE_CONFIG_URL"],
|
||||
base_url=os.environ.get("BASE_URL", "http://localhost:8000"),
|
||||
)
|
||||
|
||||
# Create FastMCP server with auth
|
||||
mcp = FastMCP(name="My Descope Protected Server", auth=auth_provider)
|
||||
|
||||
```
|
||||
|
||||
### Scope discovery and validation
|
||||
|
||||
When both `scopes_supported` and `required_scopes` are omitted, `DescopeProvider` discovers `scopes_supported` lazily from the OpenID configuration and advertises them to MCP clients. Provider construction remains network-free, and a transient discovery failure is retried on a later metadata request.
|
||||
|
||||
Set both options when clients should request a broader set of scopes than the server requires on every token:
|
||||
|
||||
```python
|
||||
from fastmcp.server.auth.providers.descope import DescopeProvider
|
||||
|
||||
auth_provider = DescopeProvider(
|
||||
config_url="https://api.descope.com/v1/apps/P.../.well-known/openid-configuration",
|
||||
base_url="https://your-fastmcp-server.com",
|
||||
scopes_supported=["mcp:read", "mcp:write"],
|
||||
required_scopes=["mcp:read"],
|
||||
)
|
||||
```
|
||||
|
||||
`scopes_supported` controls what the protected resource metadata advertises. `required_scopes` controls what the JWT verifier requires during token validation. When only `required_scopes` is set, those scopes are also advertised to clients.
|
||||
|
||||
## Testing
|
||||
|
||||
To test your server, you can use the `fastmcp` CLI to run it locally. Assuming you've saved the above code to `server.py` (after replacing the environment variables with your actual values!), you can run the following command:
|
||||
|
|
@ -88,7 +116,8 @@ import asyncio
|
|||
|
||||
async def main():
|
||||
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
|
||||
assert await client.ping()
|
||||
tools = await client.list_tools()
|
||||
print(f"Authenticated. Server exposes {len(tools)} tools.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
|
|||
|
|
@ -108,7 +108,8 @@ async def main():
|
|||
print("✓ Authenticated with Discord!")
|
||||
|
||||
result = await client.call_tool("get_user_info")
|
||||
print(f"Discord user: {result['username']}")
|
||||
user_info = result.data
|
||||
print(f"Discord user: {user_info['username']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ fastmcp install gemini-cli server.py --project /path/to/my-project
|
|||
If your server needs environment variables (like API keys), you must include them:
|
||||
|
||||
```bash
|
||||
fastmcp install gemini-cli server.py --server-name "Weather Server" \
|
||||
fastmcp install gemini-cli server.py --name "Weather Server" \
|
||||
--env API_KEY=your-api-key \
|
||||
--env DEBUG=true
|
||||
```
|
||||
|
|
@ -126,7 +126,7 @@ fastmcp install gemini-cli server.py --server-name "Weather Server" \
|
|||
Or load them from a `.env` file:
|
||||
|
||||
```bash
|
||||
fastmcp install gemini-cli server.py --server-name "Weather Server" --env-file .env
|
||||
fastmcp install gemini-cli server.py --name "Weather Server" --env-file .env
|
||||
```
|
||||
|
||||
<Warning>
|
||||
|
|
|
|||
|
|
@ -130,8 +130,9 @@ async def main():
|
|||
|
||||
# Test the protected tool
|
||||
result = await client.call_tool("get_user_info")
|
||||
print(f"Google user: {result['email']}")
|
||||
print(f"Name: {result['name']}")
|
||||
user_info = result.data
|
||||
print(f"Google user: {user_info['email']}")
|
||||
print(f"Name: {user_info['name']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
|
|||
304
docs/integrations/huggingface.mdx
Normal file
304
docs/integrations/huggingface.mdx
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
---
|
||||
title: Hugging Face OAuth 🤝 FastMCP
|
||||
sidebarTitle: Hugging Face
|
||||
description: Secure your FastMCP server with Hugging Face OAuth
|
||||
icon: hugging-face
|
||||
iconType: brands
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
<VersionBadge version="3.4.3" />
|
||||
|
||||
This guide shows you how to secure your FastMCP server using **Hugging Face OAuth**.
|
||||
The `HuggingFaceProvider` uses FastMCP's [OAuth Proxy](/servers/auth/oauth-proxy)
|
||||
pattern with Hugging Face's OAuth and OpenID Connect endpoints. It works with
|
||||
manually created confidential apps, public PKCE apps, and Client ID Metadata
|
||||
Documents (CIMD).
|
||||
|
||||
When deploying your MCP server to Hugging Face Spaces, Spaces can create and
|
||||
manage the OAuth app for you.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before you begin, you will need:
|
||||
|
||||
1. A **[Hugging Face account](https://huggingface.co/join)** with access to create OAuth apps
|
||||
2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
|
||||
|
||||
### Step 1: Create a Hugging Face OAuth app
|
||||
|
||||
Create an OAuth app from your [Hugging Face application settings](https://huggingface.co/settings/applications/new).
|
||||
For details, see Hugging Face's [OAuth documentation](https://huggingface.co/docs/hub/oauth).
|
||||
|
||||
<Steps>
|
||||
<Step title="Create the OAuth app">
|
||||
Go to your [Hugging Face application settings](https://huggingface.co/settings/applications/new)
|
||||
and create a new OAuth application.
|
||||
|
||||
Choose a name users will recognize, then configure the redirect URL for
|
||||
your FastMCP server:
|
||||
|
||||
- Development: `http://localhost:8000/auth/callback`
|
||||
- Production: `https://your-domain.com/auth/callback`
|
||||
|
||||
<Warning>
|
||||
The redirect URL must match exactly. The default path is `/auth/callback`,
|
||||
but you can customize it using the `redirect_path` parameter. For
|
||||
production, use HTTPS.
|
||||
</Warning>
|
||||
</Step>
|
||||
|
||||
<Step title="Save your credentials">
|
||||
After creating the app, save:
|
||||
|
||||
- **Client ID**: The public identifier for your Hugging Face OAuth app
|
||||
- **Client Secret**: The app secret, if you created a confidential app
|
||||
|
||||
<Tip>
|
||||
Store the client secret securely. Never commit it to version control. Use
|
||||
environment variables or a secrets manager in production.
|
||||
</Tip>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Step 2: Configure FastMCP
|
||||
|
||||
```python server.py
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.huggingface import HuggingFaceProvider
|
||||
|
||||
# The HuggingFaceProvider handles Hugging Face's opaque OAuth access tokens
|
||||
# and stores user data in token claims.
|
||||
auth_provider = HuggingFaceProvider(
|
||||
client_id="your-huggingface-client-id", # Your Hugging Face OAuth app client ID
|
||||
client_secret="your-huggingface-client-secret", # Your Hugging Face OAuth app client secret
|
||||
base_url="http://localhost:8000", # Must match your OAuth configuration
|
||||
required_scopes=["openid", "profile"], # Default value
|
||||
# redirect_path="/auth/callback" # Default value, customize if needed
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Hugging Face Secured App", auth=auth_provider)
|
||||
|
||||
|
||||
# Add a protected tool to test authentication
|
||||
@mcp.tool
|
||||
async def get_user_info() -> dict:
|
||||
"""Returns information about the authenticated Hugging Face user."""
|
||||
from fastmcp.server.dependencies import get_access_token
|
||||
|
||||
token = get_access_token()
|
||||
return {
|
||||
"subject": token.claims.get("sub"),
|
||||
"username": token.claims.get("preferred_username"),
|
||||
"profile": token.claims.get("profile"),
|
||||
}
|
||||
```
|
||||
|
||||
## Public OAuth apps, DCR, and CIMD
|
||||
|
||||
Hugging Face supports public OAuth apps (no client secret). For public apps,
|
||||
omit `client_secret` and provide a `jwt_signing_key` so FastMCP can sign its
|
||||
own proxy tokens:
|
||||
|
||||
```python
|
||||
auth_provider = HuggingFaceProvider(
|
||||
client_id="your-public-huggingface-client-id",
|
||||
base_url="http://localhost:8000",
|
||||
jwt_signing_key="replace-with-a-secure-secret",
|
||||
)
|
||||
```
|
||||
|
||||
MCP clients can use Dynamic Client Registration with your FastMCP server. The
|
||||
`HuggingFaceProvider` inherits FastMCP's OAuth Proxy behavior, which handles
|
||||
client registration locally and forwards authorization to Hugging Face using
|
||||
your configured Hugging Face OAuth app. In other words, MCP clients register
|
||||
with FastMCP, while FastMCP uses your Hugging Face `client_id` and optional
|
||||
`client_secret` for the upstream OAuth flow.
|
||||
|
||||
You can also use a Client ID Metadata Document URL as the `client_id` when your
|
||||
client metadata is hosted at a stable HTTPS URL:
|
||||
|
||||
```python
|
||||
auth_provider = HuggingFaceProvider(
|
||||
client_id="https://your-client.example/.well-known/oauth-cimd",
|
||||
base_url="http://localhost:8000",
|
||||
jwt_signing_key="replace-with-a-secure-secret",
|
||||
)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Running the Server
|
||||
|
||||
Start your server with HTTP transport:
|
||||
|
||||
```bash
|
||||
fastmcp run server.py --transport http --port 8000
|
||||
```
|
||||
|
||||
Your server is now running and protected by Hugging Face OAuth authentication.
|
||||
|
||||
### Testing with a Client
|
||||
|
||||
Create a test client that authenticates with your Hugging Face-protected server:
|
||||
|
||||
```python test_client.py
|
||||
import asyncio
|
||||
from fastmcp import Client
|
||||
|
||||
|
||||
async def main():
|
||||
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
|
||||
result = await client.call_tool("get_user_info")
|
||||
print(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
When you run the client for the first time:
|
||||
|
||||
1. Your browser will open to Hugging Face's authorization page
|
||||
2. Sign in with your Hugging Face account and grant the requested permissions
|
||||
3. After authorization, you'll be redirected back
|
||||
4. The client receives the token and can make authenticated requests
|
||||
|
||||
<Info>
|
||||
The client caches tokens locally, so you won't need to re-authenticate for
|
||||
subsequent runs unless the token expires or you explicitly clear the cache.
|
||||
</Info>
|
||||
|
||||
## Hugging Face Spaces
|
||||
|
||||
When deploying to [Hugging Face Spaces](https://huggingface.co/docs/hub/spaces-oauth),
|
||||
Spaces can create and manage the OAuth app for you. Add OAuth metadata to your
|
||||
Space README:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: FastMCP Hugging Face OAuth
|
||||
sdk: docker
|
||||
hf_oauth: true
|
||||
hf_oauth_expiration_minutes: 480
|
||||
hf_oauth_scopes:
|
||||
- email
|
||||
- inference-api
|
||||
---
|
||||
```
|
||||
|
||||
Spaces provide `OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET`, `OAUTH_SCOPES`,
|
||||
`OPENID_PROVIDER_URL`, and `SPACE_HOST` environment variables:
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.huggingface import HuggingFaceProvider
|
||||
from fastmcp.utilities.auth import parse_scopes
|
||||
|
||||
base_url = f"https://{os.environ['SPACE_HOST']}"
|
||||
|
||||
auth_provider = HuggingFaceProvider(
|
||||
client_id=os.environ["OAUTH_CLIENT_ID"],
|
||||
client_secret=os.environ["OAUTH_CLIENT_SECRET"],
|
||||
base_url=base_url,
|
||||
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
|
||||
required_scopes=parse_scopes(os.environ.get("OAUTH_SCOPES")) or ["openid", "profile"],
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Hugging Face Space App", auth=auth_provider)
|
||||
```
|
||||
|
||||
Set `JWT_SIGNING_KEY` as a Space secret.
|
||||
|
||||
## Hugging Face scopes
|
||||
|
||||
The default scopes are `openid` and `profile`. Add more scopes when your tools
|
||||
need Hub capabilities:
|
||||
|
||||
| Scope | Description |
|
||||
|-------|-------------|
|
||||
| `email` | Access the user's email address |
|
||||
| `read-billing` | Know whether the user has a payment method set up |
|
||||
| `read-repos` | Read the user's personal repositories |
|
||||
| `gated-repos` | Read public gated repositories the user can access |
|
||||
| `contribute-repos` | Create repositories and access app-created repositories |
|
||||
| `write-repos` | Read and write the user's personal repositories |
|
||||
| `manage-repos` | Full repository access, including creation and deletion |
|
||||
| `read-collections` | Read the user's personal collections |
|
||||
| `write-collections` | Read and write the user's personal collections, including collection creation and deletion |
|
||||
| `inference-api` | Use Hugging Face Inference Providers as the user |
|
||||
| `jobs` | Run Hugging Face Jobs |
|
||||
| `webhooks` | Manage webhooks |
|
||||
| `write-discussions` | Open discussions and pull requests, and interact with discussions |
|
||||
|
||||
```python
|
||||
auth_provider = HuggingFaceProvider(
|
||||
client_id="your-huggingface-client-id",
|
||||
client_secret="your-huggingface-client-secret",
|
||||
base_url="https://your-domain.com",
|
||||
required_scopes=["openid", "profile", "inference-api", "jobs"],
|
||||
)
|
||||
```
|
||||
|
||||
For organization resources, use Hugging Face's normal OAuth organization grant
|
||||
flow. If you need a specific organization, pass Hugging Face's `orgIds`
|
||||
authorization parameter. The value is the organization ID from the
|
||||
`organizations.sub` field in the Hugging Face userinfo response:
|
||||
|
||||
```python
|
||||
auth_provider = HuggingFaceProvider(
|
||||
client_id="your-huggingface-client-id",
|
||||
client_secret="your-huggingface-client-secret",
|
||||
base_url="https://your-domain.com",
|
||||
extra_authorize_params={"orgIds": "your-org-id"},
|
||||
)
|
||||
```
|
||||
|
||||
## Production Configuration
|
||||
|
||||
For production deployments with persistent token management across server
|
||||
restarts, configure `jwt_signing_key` and `client_storage`:
|
||||
|
||||
```python server.py
|
||||
import os
|
||||
from cryptography.fernet import Fernet
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.huggingface import HuggingFaceProvider
|
||||
from key_value.aio.stores.redis import RedisStore
|
||||
from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
|
||||
|
||||
# Production setup with encrypted persistent token storage
|
||||
auth_provider = HuggingFaceProvider(
|
||||
client_id="your-huggingface-client-id",
|
||||
client_secret=os.environ["HUGGINGFACE_CLIENT_SECRET"],
|
||||
base_url="https://your-production-domain.com",
|
||||
required_scopes=["openid", "profile", "email"],
|
||||
|
||||
# Production token management
|
||||
jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
|
||||
client_storage=FernetEncryptionWrapper(
|
||||
key_value=RedisStore(
|
||||
host=os.environ["REDIS_HOST"],
|
||||
port=int(os.environ["REDIS_PORT"])
|
||||
),
|
||||
fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
|
||||
)
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Production Hugging Face App", auth=auth_provider)
|
||||
```
|
||||
|
||||
<Note>
|
||||
Parameters (`jwt_signing_key` and `client_storage`) work together to ensure
|
||||
tokens and client registrations survive server restarts. **Wrap your storage in
|
||||
`FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without
|
||||
it, tokens are stored in plaintext. Store secrets in environment variables and
|
||||
use a persistent storage backend like Redis for distributed deployments.
|
||||
|
||||
For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
|
||||
</Note>
|
||||
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
|
@ -75,7 +75,7 @@ Follow the Steps as mentioned below to create an OAuth client.
|
|||
Click on "Edit OAuth configuration" button.
|
||||
Configure the application as OAuth client by selecting "Configure this application as a client now" radio button.
|
||||
Select "Authorization code" grant type. If you are planning to use the same OAuth client application for token exchange, select "Client credentials" grant type as well. In the sample, we will use the same client.
|
||||
For Authorization grant type, select redirect URL. In most cases, this will be the MCP server URL followed by "/oauth/callback".
|
||||
For Authorization grant type, select redirect URL. In most cases, this will be the MCP server URL followed by "/auth/callback".
|
||||
|
||||
<Frame>
|
||||
<img src="/integrations/images/oci/ocioauthconfiguration.png" alt="OAuth Configuration for an Integrated Application in OCI IAM Domain" />
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ We recommend using the FastAPI integration for bootstrapping and prototyping, no
|
|||
To convert an OpenAPI specification to an MCP server, use the `FastMCP.from_openapi()` class method:
|
||||
|
||||
```python server.py
|
||||
import httpx
|
||||
import httpx2
|
||||
from fastmcp import FastMCP
|
||||
|
||||
# Create an HTTP client for your API
|
||||
client = httpx.AsyncClient(base_url="https://api.example.com")
|
||||
client = httpx2.AsyncClient(base_url="https://api.example.com")
|
||||
|
||||
# Load your OpenAPI spec
|
||||
openapi_spec = httpx.get("https://api.example.com/openapi.json").json()
|
||||
openapi_spec = httpx2.get("https://api.example.com/openapi.json").json()
|
||||
|
||||
# Create the MCP server
|
||||
mcp = FastMCP.from_openapi(
|
||||
|
|
@ -51,20 +51,19 @@ if __name__ == "__main__":
|
|||
If your API requires authentication, configure it on the HTTP client:
|
||||
|
||||
```python
|
||||
import httpx
|
||||
import httpx2
|
||||
from fastmcp import FastMCP
|
||||
|
||||
# Bearer token authentication
|
||||
api_client = httpx.AsyncClient(
|
||||
api_client = httpx2.AsyncClient(
|
||||
base_url="https://api.example.com",
|
||||
headers={"Authorization": "Bearer YOUR_TOKEN"}
|
||||
)
|
||||
|
||||
# Create MCP server with authenticated client
|
||||
mcp = FastMCP.from_openapi(
|
||||
openapi_spec=spec,
|
||||
openapi_spec=spec,
|
||||
client=api_client,
|
||||
timeout=30.0 # 30 second timeout for all requests
|
||||
)
|
||||
```
|
||||
|
||||
|
|
@ -404,7 +403,7 @@ FastMCP intelligently handles different types of parameters in OpenAPI requests:
|
|||
|
||||
### Query Parameters
|
||||
|
||||
By default, FastMCP only includes query parameters that have non-empty values. Parameters with `None` values or empty strings are automatically filtered out.
|
||||
By default, FastMCP skips parameters whose value is `None`. Empty strings are still sent as empty query values, which is useful for APIs that distinguish between an omitted parameter and an explicitly blank one.
|
||||
|
||||
```python
|
||||
# When calling this tool...
|
||||
|
|
@ -412,10 +411,10 @@ await client.call_tool("search_products", {
|
|||
"category": "electronics", # ✅ Included
|
||||
"min_price": 100, # ✅ Included
|
||||
"max_price": None, # ❌ Excluded
|
||||
"brand": "", # ❌ Excluded
|
||||
"brand": "", # ✅ Included as an empty value
|
||||
})
|
||||
|
||||
# The HTTP request will be: GET /products?category=electronics&min_price=100
|
||||
# The HTTP request will be: GET /products?category=electronics&min_price=100&brand=
|
||||
```
|
||||
|
||||
### Path Parameters
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ The middleware automatically maps MCP methods to Permit.io resources and actions
|
|||
> **Note:**
|
||||
> Don't forget to assign the relevant role (e.g., Admin, User) to the user authenticating to your MCP server (such as the user in the JWT) in the Permit.io Directory. Without the correct role assignment, users will not have access to the resources and actions you've configured in your policies.
|
||||
>
|
||||
> 
|
||||
> 
|
||||
>
|
||||
> *Example: In Permit.io Directory, both 'client' and 'admin' users are assigned the 'Admin' role, granting them the permissions defined in your policy mapping.*
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,8 @@ import asyncio
|
|||
|
||||
async def main():
|
||||
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
|
||||
assert await client.ping()
|
||||
tools = await client.list_tools()
|
||||
print(f"Authenticated. Server exposes {len(tools)} tools.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@ In your Scalekit dashboard:
|
|||
2. Enter server details: a name, a resource identifier, and the desired MCP client authentication settings
|
||||
3. Save, then copy the **Resource ID** (for example, res_92015146095)
|
||||
|
||||
In your FastMCP project's `.env`:
|
||||
Record these values in a `.env` file in your FastMCP project:
|
||||
|
||||
```sh
|
||||
SCALEKIT_ENVIRONMENT_URL=<YOUR_APP_ENVIRONMENT_URL>
|
||||
SCALEKIT_RESOURCE_ID=<YOUR_APP_RESOURCE_ID> # res_926EXAMPLE5878
|
||||
BASE_URL=http://localhost:8000/
|
||||
```sh .env
|
||||
SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com
|
||||
SCALEKIT_RESOURCE_ID=res_926EXAMPLE5878
|
||||
BASE_URL=http://localhost:8000
|
||||
# Optional: additional scopes tokens must have
|
||||
# SCALEKIT_REQUIRED_SCOPES=read,write
|
||||
```
|
||||
|
|
@ -43,20 +43,25 @@ BASE_URL=http://localhost:8000/
|
|||
|
||||
### Step 2: Add auth to FastMCP server
|
||||
|
||||
Create your FastMCP server file and use the ScalekitProvider to handle all the OAuth integration automatically:
|
||||
Create your FastMCP server file and use the ScalekitProvider to handle all the OAuth integration automatically. Nothing reads `.env` automatically, so load it explicitly with [python-dotenv](https://pypi.org/project/python-dotenv/) (`pip install python-dotenv`) before constructing the provider.
|
||||
|
||||
> **Warning:** The legacy `mcp_url` and `client_id` parameters are deprecated and will be removed in a future release. Use `base_url` instead of `mcp_url` and remove `client_id` from your configuration.
|
||||
|
||||
```python server.py
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.scalekit import ScalekitProvider
|
||||
|
||||
# Discovers Scalekit endpoints and set up JWT token validation
|
||||
load_dotenv()
|
||||
|
||||
# Discovers Scalekit endpoints and sets up JWT token validation
|
||||
auth_provider = ScalekitProvider(
|
||||
environment_url=SCALEKIT_ENVIRONMENT_URL, # Scalekit environment URL
|
||||
resource_id=SCALEKIT_RESOURCE_ID, # Resource server ID
|
||||
base_url=SERVER_URL, # Public MCP endpoint
|
||||
required_scopes=["read"], # Optional scope enforcement
|
||||
environment_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], # Scalekit environment URL
|
||||
resource_id=os.environ["SCALEKIT_RESOURCE_ID"], # Resource server ID
|
||||
base_url=os.environ.get("BASE_URL", "http://localhost:8000"),
|
||||
required_scopes=["read"], # Optional scope enforcement
|
||||
)
|
||||
|
||||
# Create FastMCP server with auth
|
||||
|
|
@ -86,7 +91,7 @@ Set `required_scopes` when you need tokens to carry specific permissions. Leave
|
|||
uv run python server.py
|
||||
```
|
||||
|
||||
Use any MCP client (for example, mcp-inspector, Claude, VS Code, or Windsurf) to connect to the running serve. Verify that authentication succeeds and requests are authorized as expected.
|
||||
Use any MCP client (for example, mcp-inspector, Claude, VS Code, or Windsurf) to connect to the running server. Verify that authentication succeeds and requests are authorized as expected.
|
||||
|
||||
## Production Configuration
|
||||
|
||||
|
|
@ -99,8 +104,8 @@ from fastmcp.server.auth.providers.scalekit import ScalekitProvider
|
|||
|
||||
# Load configuration from environment variables
|
||||
auth = ScalekitProvider(
|
||||
environment_url=os.environ.get("SCALEKIT_ENVIRONMENT_URL"),
|
||||
resource_id=os.environ.get("SCALEKIT_RESOURCE_ID"),
|
||||
environment_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
|
||||
resource_id=os.environ["SCALEKIT_RESOURCE_ID"],
|
||||
base_url=os.environ.get("BASE_URL", "https://your-server.com")
|
||||
)
|
||||
|
||||
|
|
@ -134,22 +139,15 @@ logging.basicConfig(level=logging.DEBUG)
|
|||
You can inspect JWT tokens in your tools to understand the user context:
|
||||
|
||||
```python
|
||||
from fastmcp.server.context import request_ctx
|
||||
import jwt
|
||||
from fastmcp.server.dependencies import get_access_token
|
||||
|
||||
@mcp.tool
|
||||
def inspect_token() -> dict:
|
||||
"""Inspect the current JWT token claims."""
|
||||
context = request_ctx.get()
|
||||
token = get_access_token()
|
||||
if token is None:
|
||||
return {"error": "No token found"}
|
||||
|
||||
# Extract token from Authorization header
|
||||
if hasattr(context, 'request') and hasattr(context.request, 'headers'):
|
||||
auth_header = context.request.headers.get('authorization', '')
|
||||
if auth_header.startswith('Bearer '):
|
||||
token = auth_header[7:]
|
||||
# Decode without verification (already verified by provider)
|
||||
claims = jwt.decode(token, options={"verify_signature": False})
|
||||
return claims
|
||||
|
||||
return {"error": "No token found"}
|
||||
# Claims were already verified by the auth provider.
|
||||
return token.claims
|
||||
```
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ Before you begin, you will need:
|
|||
2. **OAuth Server enabled** in your Supabase Dashboard (Authentication → OAuth Server)
|
||||
3. **Dynamic Client Registration enabled** in the same settings
|
||||
4. A **consent UI** hosted at your configured authorization path (see above)
|
||||
5. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
|
||||
5. Your Supabase Auth JWT signing algorithm. `SupabaseProvider` defaults to `ES256`; set `algorithm="RS256"` if your project is configured for RSA signing.
|
||||
6. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
|
||||
|
||||
### Step 1: Enable Supabase OAuth Server
|
||||
|
||||
|
|
@ -58,6 +59,7 @@ from fastmcp.server.auth.providers.supabase import SupabaseProvider
|
|||
auth = SupabaseProvider(
|
||||
project_url="https://abc123.supabase.co",
|
||||
base_url="http://localhost:8000",
|
||||
algorithm="ES256", # Match your Supabase Auth JWT signing algorithm
|
||||
)
|
||||
|
||||
mcp = FastMCP("Supabase Protected Server", auth=auth)
|
||||
|
|
@ -117,6 +119,7 @@ from fastmcp.server.auth.providers.supabase import SupabaseProvider
|
|||
auth = SupabaseProvider(
|
||||
project_url=os.environ["SUPABASE_PROJECT_URL"],
|
||||
base_url=os.environ.get("BASE_URL", "https://your-server.com"),
|
||||
algorithm=os.environ.get("SUPABASE_JWT_ALGORITHM", "ES256"),
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Supabase Secured App", auth=auth)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ description: Configure FastMCP behavior through environment variables or a .env
|
|||
icon: gear
|
||||
---
|
||||
|
||||
FastMCP uses [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) for configuration. Every setting is available as an environment variable with a `FASTMCP_` prefix. Settings are loaded from environment variables and from a `.env` file (see the [Tasks (Docket)](#tasks-docket) section for a caveat about nested settings in `.env` files).
|
||||
FastMCP uses [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) for configuration. Every setting is available as an environment variable with a `FASTMCP_` prefix. Settings are loaded from environment variables and from a `.env` file.
|
||||
|
||||
```bash
|
||||
# Set via environment
|
||||
|
|
@ -27,6 +27,7 @@ You can change which `.env` file is loaded by setting the `FASTMCP_ENV_FILE` env
|
|||
| `FASTMCP_ENABLE_RICH_LOGGING` | `bool` | `true` | Use rich formatting for log output. Set to `false` for plain Python logging. |
|
||||
| `FASTMCP_ENABLE_RICH_TRACEBACKS` | `bool` | `true` | Use rich tracebacks for errors. |
|
||||
| `FASTMCP_DEPRECATION_WARNINGS` | `bool` | `true` | Show deprecation warnings. |
|
||||
| `FASTMCP_MCP_CAMELCASE_COMPAT` | `bool` | `true` | Bridge legacy camelCase reads on MCP SDK objects (e.g. `tool.inputSchema`, `result.isError`) to their snake_case fields after the SDK v2 rename. Each bridged read emits a `FastMCPDeprecationWarning`. Set to `false` to disable the shims, in which case only the snake_case names resolve. |
|
||||
|
||||
## Transport & HTTP
|
||||
|
||||
|
|
@ -42,9 +43,10 @@ These control how the server listens when running with an HTTP transport.
|
|||
| `FASTMCP_STREAMABLE_HTTP_PATH` | `str` | `/mcp` | Path for Streamable HTTP endpoint. |
|
||||
| `FASTMCP_STATELESS_HTTP` | `bool` | `false` | Enable stateless HTTP mode (new transport per request). Useful for multi-worker deployments. |
|
||||
| `FASTMCP_JSON_RESPONSE` | `bool` | `false` | Use JSON responses instead of SSE for Streamable HTTP. |
|
||||
| `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION` | `bool` | `true` | Validate `Host` and browser `Origin` headers for Streamable HTTP requests. |
|
||||
| `FASTMCP_HTTP_ALLOWED_HOSTS` | `list[str] \| null` | `null` | Additional trusted hostnames for Streamable HTTP requests. Use a JSON array, such as `["mcp.example.com"]`. |
|
||||
| `FASTMCP_HTTP_ALLOWED_ORIGINS` | `list[str] \| null` | `null` | Browser origins trusted by the Streamable HTTP request guard. Configure CORS separately for cross-origin browser reads. Use a JSON array, such as `["https://app.example.com"]`. |
|
||||
| `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION` | `bool \| "auto"` | `false` | Validate `Host` and browser `Origin` headers for Streamable HTTP requests. `auto` protects localhost-bound servers and explicit host/origin allowlists. |
|
||||
| `FASTMCP_HTTP_ALLOWED_HOSTS` | `list[str] \| null` | `null` | Additional trusted hostnames when Host and Origin protection is enabled. Use a JSON array, such as `["mcp.example.com"]`. |
|
||||
| `FASTMCP_HTTP_ALLOWED_ORIGINS` | `list[str] \| null` | `null` | Browser origins trusted when Host and Origin protection is enabled. Configure CORS separately for cross-origin browser reads. Use a JSON array, such as `["https://app.example.com"]`. |
|
||||
| `FASTMCP_HTTP_SESSION_IDLE_TIMEOUT` | `float \| null` | `null` | Seconds a Streamable HTTP session may remain idle before it is terminated. The deadline resets on every request. When `null`, sessions never expire from inactivity. Not supported in stateless mode. |
|
||||
| `FASTMCP_DEBUG` | `bool` | `false` | Enable debug mode. |
|
||||
|
||||
## Error Handling
|
||||
|
|
@ -61,6 +63,7 @@ These control how the server listens when running with an HTTP transport.
|
|||
|---|---|---|---|
|
||||
| `FASTMCP_CLIENT_INIT_TIMEOUT` | `float \| None` | None | Timeout in seconds for the client initialization handshake. Set to `0` or leave unset to disable. |
|
||||
| `FASTMCP_CLIENT_DISCONNECT_TIMEOUT` | `float` | `5` | Maximum time in seconds to wait for a clean disconnect before giving up. |
|
||||
| `FASTMCP_TASKS_CLIENT_POLL_INTERVAL` | `float` | `0.5` | Ceiling in seconds for the fallback poll backoff while waiting on a [background task](/servers/tasks). Requires the `fastmcp-tasks` package. Applies **only** when the server does not advertise its own `pollInterval`: in that case `Task.wait()` starts polling fast (~20ms) and doubles up to this ceiling rather than polling at a fixed cadence. When the server advertises a `pollInterval`, that interval is honored exactly and this setting is ignored. |
|
||||
| `FASTMCP_CLIENT_RAISE_FIRST_EXCEPTIONGROUP_ERROR` | `bool` | `true` | When an `ExceptionGroup` is raised, re-raise the first error directly instead of the group. Simplifies debugging but may mask secondary errors. |
|
||||
|
||||
## CLI & Display
|
||||
|
|
@ -70,23 +73,35 @@ These control how the server listens when running with an HTTP transport.
|
|||
| `FASTMCP_SHOW_SERVER_BANNER` | `bool` | `true` | Show the server banner on startup. Also controllable via `--no-banner` or `server.run(show_banner=False)`. |
|
||||
| `FASTMCP_CHECK_FOR_UPDATES` | `Literal["stable", "prerelease", "off"]` | `stable` | Update checking on CLI startup. `stable` checks stable releases only, `prerelease` includes pre-releases, `off` disables checking. |
|
||||
|
||||
## Tasks (Docket)
|
||||
|
||||
These configure the [Docket](https://github.com/prefecthq/docket) task queue used by [server tasks](/servers/tasks). All use the `FASTMCP_DOCKET_` prefix.
|
||||
|
||||
<Warning>
|
||||
When setting Docket values in a `.env` file, use a **double** underscore: `FASTMCP_DOCKET__URL` (not `FASTMCP_DOCKET_URL`). This is because `.env` values are resolved through the parent `Settings` class, which uses `__` as its nested delimiter. As regular environment variables (e.g., `export`), the single-underscore form `FASTMCP_DOCKET_URL` works fine.
|
||||
</Warning>
|
||||
## Telemetry
|
||||
|
||||
| Environment Variable | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `FASTMCP_DOCKET_NAME` | `str` | `fastmcp` | Queue name. Servers and workers sharing the same name and backend URL share a task queue. |
|
||||
| `FASTMCP_DOCKET_URL` | `str` | `memory://` | Backend URL. Use `memory://` for single-process or `redis://host:port/db` for distributed workers. |
|
||||
| `FASTMCP_DOCKET_WORKER_NAME` | `str \| None` | None | Worker name. Auto-generated if unset. |
|
||||
| `FASTMCP_DOCKET_CONCURRENCY` | `int` | `10` | Maximum concurrent tasks per worker. |
|
||||
| `FASTMCP_DOCKET_REDELIVERY_TIMEOUT` | `timedelta` | `300s` | If a worker doesn't complete a task within this time, it's redelivered to another worker. |
|
||||
| `FASTMCP_DOCKET_RECONNECTION_DELAY` | `timedelta` | `5s` | Delay between reconnection attempts when the worker loses its backend connection. |
|
||||
| `FASTMCP_DOCKET_MINIMUM_CHECK_INTERVAL` | `timedelta` | `50ms` | How frequently the worker polls for new tasks. Lower values reduce latency at the cost of more CPU usage. |
|
||||
| `FASTMCP_ENABLE_TELEMETRY` | `bool` | `true` | Whether FastMCP's native [OpenTelemetry instrumentation](/servers/telemetry) is active. Enabled by default; FastMCP uses only the OpenTelemetry API, so span creation is a no-op with negligible overhead unless an OpenTelemetry SDK and exporter are configured. Set to `false` to turn instrumentation off entirely, in which case no FastMCP spans are created even when an SDK is configured. |
|
||||
|
||||
## Tasks (Docket)
|
||||
|
||||
Task settings (the `FASTMCP_DOCKET_` variables) moved to the optional `fastmcp-tasks` package. See [server tasks](/servers/tasks) for configuration.
|
||||
|
||||
## Security
|
||||
|
||||
These control FastMCP's SSRF protection for the outbound fetches it makes during authentication (OAuth client metadata and JWKS).
|
||||
|
||||
| Environment Variable | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `FASTMCP_SSRF_TRUST_PROXY` | `bool` | `false` | Trust an outbound HTTP proxy for SSRF-protected fetches. When `false`, FastMCP resolves the target hostname itself and refuses to connect if it maps to a private, loopback, link-local, or reserved IP. When `true`, FastMCP routes auth metadata and JWKS fetches through the configured `HTTPS_PROXY`/`ALL_PROXY` and does not honor `NO_PROXY`; if no proxy is configured the fetch is refused. |
|
||||
|
||||
By default, FastMCP protects its OAuth and JWKS fetches against [SSRF](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery) by resolving the target hostname, rejecting any address that maps to a private, loopback, link-local, or reserved IP, and then pinning the connection to that validated IP.
|
||||
|
||||
This breaks when a corporate `CONNECT` proxy is the only egress path: the container often cannot resolve external DNS at all (only the proxy can), and even when it can, pinning to the IP makes TLS verification fail because public certificates list hostnames, not IP addresses.
|
||||
|
||||
Set `FASTMCP_SSRF_TRUST_PROXY=true` when a trusted proxy is your mandated egress. FastMCP then skips DNS resolution and the IP blocklist entirely and makes a single request to the hostname URL, explicitly routed through the proxy named by the standard `HTTPS_PROXY` / `ALL_PROXY` environment variables (checked in that order). The HTTPS-only and hostname checks still apply.
|
||||
|
||||
<Warning>
|
||||
This is a deliberate trust shift: the IP blocklist cannot be enforced through a proxy (the proxy does its own DNS, so an address FastMCP resolved is not the one the proxy dials). Only enable it when the proxy itself is trusted to mediate egress.
|
||||
|
||||
FastMCP reads the proxy URL from the environment and passes it to the HTTP client explicitly, with the client's own environment-based proxy routing turned off — so the request either goes through that exact proxy or fails outright, with no routing decision left for the client to make on its own. One consequence: `NO_PROXY` is **not honored** in this mode. A host that `NO_PROXY` would otherwise exclude is still routed through the configured proxy rather than fetched direct with the IP blocklist disabled — the safer of the two options, since the blocklist cannot apply to a direct fetch here anyway. If you set `FASTMCP_SSRF_TRUST_PROXY=true` but neither `HTTPS_PROXY` nor `ALL_PROXY` is present in the server process's environment (an `HTTP_PROXY` alone never routes these HTTPS-only fetches), the request would otherwise go out **direct with the IP blocklist disabled** — no SSRF protection at all. Rather than send it, FastMCP refuses the fetch and raises `SSRFError` with an actionable message. The contract is crisp: proxy-trust mode delegates SSRF protection to the proxy, and with no proxy configured the fetch cannot proceed. Enable this setting only together with an active proxy that routes your auth endpoints.
|
||||
</Warning>
|
||||
|
||||
## Advanced
|
||||
|
||||
|
|
@ -95,5 +110,4 @@ When setting Docket values in a `.env` file, use a **double** underscore: `FASTM
|
|||
| `FASTMCP_HOME` | `Path` | Platform default | Data directory for FastMCP. Defaults to the platform-specific user data directory. |
|
||||
| `FASTMCP_ENV_FILE` | `str` | `.env` | Path to the `.env` file to load settings from. Must be set as an environment variable (see above). |
|
||||
| `FASTMCP_SERVER_DEPENDENCIES` | `list[str]` | `[]` | Additional dependencies to install in the server environment. |
|
||||
| `FASTMCP_DECORATOR_MODE` | `Literal["function", "object"]` | `function` | Controls what `@tool`, `@resource`, and `@prompt` decorators return. `function` returns the original function (default); `object` returns component objects (deprecated, will be removed). |
|
||||
| `FASTMCP_TEST_MODE` | `bool` | `false` | Enable test mode. |
|
||||
|
|
|
|||
|
|
@ -30,12 +30,12 @@ from fastmcp.contrib import my_module
|
|||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions to the `contrib` package! If you have a module that extends FastMCP in a useful way, consider contributing it:
|
||||
Contrib modules are accepted selectively. Before opening a PR, first open an issue with the problem, intended maintenance model, and why the pattern belongs in-repo instead of a standalone package. If maintainers agree it belongs in `contrib`, prepare the module with:
|
||||
|
||||
1. Create a new directory in `fastmcp_slim/fastmcp/contrib/` for your module
|
||||
3. Add proper tests for your module in `tests/contrib/`
|
||||
2. Include comprehensive documentation in a README.md file, including usage and examples, as well as any additional dependencies or installation instructions
|
||||
5. Submit a pull request
|
||||
2. Add proper tests for your module in `tests/contrib/`
|
||||
3. Include comprehensive documentation in a README.md file, including usage and examples, as well as any additional dependencies or installation instructions
|
||||
4. Submit a focused pull request linked to the maintainer-approved issue
|
||||
|
||||
The ideal contrib module:
|
||||
- Solves a specific use case or integration need
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
"group": "fastmcp.utilities",
|
||||
"pages": [
|
||||
"python-sdk/fastmcp-utilities-__init__",
|
||||
"python-sdk/fastmcp-utilities-asgi_transport",
|
||||
"python-sdk/fastmcp-utilities-async_utils",
|
||||
"python-sdk/fastmcp-utilities-auth",
|
||||
"python-sdk/fastmcp-utilities-authorization",
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ Usage::
|
|||
|
||||
## Classes
|
||||
|
||||
### `FastMCPApp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L144" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `FastMCPApp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L145" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A Provider that represents an MCP application.
|
||||
|
|
@ -48,19 +48,19 @@ can find them by original name even when transforms have been applied.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L168" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L169" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
tool(self, name_or_fn: F) -> F
|
||||
```
|
||||
|
||||
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L180" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L181" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
tool(self, name_or_fn: str | None = None) -> Callable[[F], F]
|
||||
```
|
||||
|
||||
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L191" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L192" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
tool(self, name_or_fn: str | AnyFunction | None = None) -> Any
|
||||
|
|
@ -83,19 +83,19 @@ Supports multiple calling patterns::
|
|||
def save(name: str): ...
|
||||
|
||||
|
||||
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L258" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L259" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
ui(self, name_or_fn: F) -> F
|
||||
```
|
||||
|
||||
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L273" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L274" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
ui(self, name_or_fn: str | None = None) -> Callable[[F], F]
|
||||
```
|
||||
|
||||
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L287" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L288" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
ui(self, name_or_fn: str | AnyFunction | None = None) -> Any
|
||||
|
|
@ -119,7 +119,7 @@ Supports multiple calling patterns::
|
|||
def dashboard() -> Component: ...
|
||||
|
||||
|
||||
#### `add_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L362" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `add_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L363" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_tool(self, tool: Tool | Callable[..., Any]) -> Tool
|
||||
|
|
@ -130,13 +130,13 @@ Add a tool to this app programmatically.
|
|||
The tool is tagged with this app's name for routing.
|
||||
|
||||
|
||||
#### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L418" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L419" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
lifespan(self) -> AsyncIterator[None]
|
||||
```
|
||||
|
||||
#### `run` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L426" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `run` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L427" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
run(self, transport: Literal['stdio', 'http', 'sse', 'streamable-http'] | None = None, **kwargs: Any) -> None
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ This module re-exports dependency injection symbols to provide a clean,
|
|||
centralized import location for all dependency-related functionality.
|
||||
|
||||
DI features (Depends, CurrentContext, CurrentFastMCP) work without pydocket
|
||||
using the uncalled-for DI engine. Only task-related dependencies (CurrentDocket,
|
||||
CurrentWorker) and background task execution require fastmcp[tasks].
|
||||
using the uncalled-for DI engine. The docket-specific dependencies
|
||||
(``CurrentDocket``, ``CurrentWorker``) live in the ``fastmcp-tasks`` package
|
||||
(``fastmcp_tasks.dependencies``).
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,37 @@ sidebarTitle: exceptions
|
|||
|
||||
Custom exceptions for FastMCP.
|
||||
|
||||
## Functions
|
||||
|
||||
### `to_mcp_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L98" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
to_mcp_error(exc: Exception) -> MCPError
|
||||
```
|
||||
|
||||
|
||||
Translate a FastMCP exception into a wire-format ``MCPError``.
|
||||
|
||||
Central mapping from FastMCP's public exception types to the JSON-RPC error
|
||||
codes defined by the MCP spec (imported from ``mcp_types``). Request-handler
|
||||
adapters call this instead of hand-rolling ``MCPError(code=..., ...)`` per
|
||||
call site, so the wire codes stay spec-correct and consistent across
|
||||
resources, prompts, and tools.
|
||||
|
||||
``NotFoundError`` and ``DisabledError`` map to ``INVALID_PARAMS`` (-32602):
|
||||
per SEP-2164 a request naming a component that does not exist (or is
|
||||
disabled) is an invalid-params error, which matches the SDK's own
|
||||
``ResourceNotFoundError -> INVALID_PARAMS`` mapping in ``mcp.server.mcpserver``.
|
||||
``ValidationError`` is also an invalid-params error. Everything else falls
|
||||
back to ``default_code`` (``INTERNAL_ERROR`` by default).
|
||||
|
||||
If ``exc`` is already an ``MCPError``, it is returned unchanged so an
|
||||
explicit code chosen upstream survives translation.
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `FastMCPDeprecationWarning` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L13" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `FastMCPDeprecationWarning` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Deprecation warning for FastMCP APIs.
|
||||
|
|
@ -20,61 +48,73 @@ still apply, but FastMCP can selectively enable its own warnings
|
|||
without affecting other libraries in the process.
|
||||
|
||||
|
||||
### `FastMCPError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L22" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `FastMCPError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L43" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Base error for FastMCP.
|
||||
|
||||
|
||||
### `ValidationError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L30" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ValidationError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L51" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Error in validating parameters or return values.
|
||||
|
||||
|
||||
### `ResourceError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ResourceError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L55" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Error in resource operations.
|
||||
|
||||
|
||||
### `ToolError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L38" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ToolError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L59" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Error in tool operations.
|
||||
|
||||
|
||||
### `PromptError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L42" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `PromptError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Error in prompt operations.
|
||||
|
||||
|
||||
### `InvalidSignature` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L46" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `InvalidSignature` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Invalid signature for use with FastMCP.
|
||||
|
||||
|
||||
### `ClientError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L50" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ClientError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L71" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Error in client operations.
|
||||
|
||||
|
||||
### `NotFoundError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `NotFoundError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L75" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Object not found.
|
||||
|
||||
|
||||
### `DisabledError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L58" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `DisabledError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L79" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Object is disabled.
|
||||
|
||||
|
||||
### `AuthorizationError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L62" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ResourceSecurityError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L83" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A templated resource parameter failed path-security screening.
|
||||
|
||||
Subclasses ``NotFoundError`` so the read handler surfaces a
|
||||
non-leaky ``INVALID_PARAMS`` (-32602) "resource not found" error to
|
||||
the client — a traversal attempt is indistinguishable from a request
|
||||
for a resource that does not exist, and never reveals which parameter
|
||||
or policy tripped.
|
||||
|
||||
|
||||
### `AuthorizationError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L94" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Error when authorization check fails.
|
||||
|
|
|
|||
|
|
@ -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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L363" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `update_config_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L373" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L168" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `StdioMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L179" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L201" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `to_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L212" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
to_transport(self) -> StdioTransport
|
||||
```
|
||||
|
||||
### `TransformingStdioMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L213" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `TransformingStdioMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L224" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L217" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `RemoteMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L228" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L253" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `to_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L264" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L281" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `TransformingRemoteMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L291" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L292" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `MCPConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L302" 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,7 +113,7 @@ For an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `wrap_servers_at_root` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L306" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `wrap_servers_at_root` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L316" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any]
|
||||
|
|
@ -122,7 +122,7 @@ wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any]
|
|||
If there's no mcpServers key but there are server configs at root, wrap them.
|
||||
|
||||
|
||||
#### `add_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L319" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `add_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L329" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L324" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `from_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L334" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L328" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `to_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L338" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L332" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `write_to_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L342" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L338" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `from_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L348" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L348" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `CanonicalMCPConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L358" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L358" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `add_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L368" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_server(self, name: str, server: CanonicalMCPServerTypes) -> None
|
||||
|
|
|
|||
|
|
@ -7,13 +7,7 @@ sidebarTitle: settings
|
|||
|
||||
## Classes
|
||||
|
||||
### `DocketSettings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L33" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Docket worker configuration.
|
||||
|
||||
|
||||
### `Settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L136" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `Settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L32" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
FastMCP settings.
|
||||
|
|
@ -21,7 +15,7 @@ FastMCP settings.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `get_setting` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L148" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `get_setting` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L44" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_setting(self, attr: str) -> Any
|
||||
|
|
@ -31,7 +25,7 @@ Get a setting. If the setting contains one or more `__`, it will be
|
|||
treated as a nested setting.
|
||||
|
||||
|
||||
#### `set_setting` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L161" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `set_setting` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L57" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_setting(self, attr: str, value: Any) -> None
|
||||
|
|
@ -41,7 +35,7 @@ Set a setting. If the setting contains one or more `__`, it will be
|
|||
treated as a nested setting.
|
||||
|
||||
|
||||
#### `normalize_log_level` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L183" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `normalize_log_level` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L79" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
normalize_log_level(cls, v)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ Example usage with SDK:
|
|||
|
||||
## Functions
|
||||
|
||||
### `get_tracer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L38" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `get_tracer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L81" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_tracer(version: str | None = None) -> Tracer
|
||||
|
|
@ -40,14 +40,23 @@ get_tracer(version: str | None = None) -> Tracer
|
|||
|
||||
Get the FastMCP tracer for creating spans.
|
||||
|
||||
Instrumentation is on by default. FastMCP uses only the OpenTelemetry API,
|
||||
so span creation is a no-op with negligible overhead unless an OpenTelemetry
|
||||
SDK and exporter are configured. Set `fastmcp.settings.enable_telemetry` to
|
||||
False (env `FASTMCP_ENABLE_TELEMETRY=false`) to turn instrumentation off
|
||||
entirely, in which case this returns a pass-through tracer that leaves the
|
||||
current OTel context untouched even when an SDK is configured.
|
||||
|
||||
**Args:**
|
||||
- `version`: Optional version string for the instrumentation
|
||||
|
||||
**Returns:**
|
||||
- A tracer instance. Returns a no-op tracer if no SDK is configured.
|
||||
- A tracer instance. Returns a non-attaching pass-through tracer if
|
||||
- telemetry is disabled; span creation is otherwise a no-op unless an SDK
|
||||
- is configured.
|
||||
|
||||
|
||||
### `inject_trace_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L50" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `inject_trace_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
inject_trace_context(meta: dict[str, Any] | None = None) -> dict[str, Any] | None
|
||||
|
|
@ -64,7 +73,7 @@ Inject current trace context into a meta dict for MCP request propagation.
|
|||
- or None if no trace context to inject and meta was None
|
||||
|
||||
|
||||
### `record_span_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L76" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `record_span_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L132" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
record_span_error(span: Span, exception: BaseException) -> None
|
||||
|
|
@ -74,7 +83,57 @@ record_span_error(span: Span, exception: BaseException) -> None
|
|||
Record an exception on a span and set error status.
|
||||
|
||||
|
||||
### `extract_trace_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L82" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `restore_dropped_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L158" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
restore_dropped_attributes(span: Span, attrs: Mapping[str, otel_types.AttributeValue]) -> None
|
||||
```
|
||||
|
||||
|
||||
Restore FastMCP attributes a non-forwarding sampler dropped entirely.
|
||||
|
||||
`Tracer.start_span` builds the span from `SamplingResult.attributes`, not
|
||||
the `attributes=` kwarg it was given for creation — a custom `Sampler`
|
||||
whose `SamplingResult.attributes` defaults to `None` silently discards
|
||||
every attribute FastMCP passed at creation time. Call this immediately
|
||||
after span creation to recover from that case.
|
||||
|
||||
The restore only fires when the span has *no* attributes at all AND the
|
||||
SDK hasn't evicted anything (`dropped_attributes == 0`):
|
||||
|
||||
- A bare, non-forwarding sampler (the regression this exists to fix)
|
||||
leaves the span with an empty attribute mapping, so everything is
|
||||
restored.
|
||||
- A sampler that supplied any attributes of its own — whether by
|
||||
forwarding ours untouched, redacting or replacing some of our values,
|
||||
or substituting its own attributes entirely (e.g. to strip component
|
||||
names or resource URIs for privacy or cardinality control) — leaves
|
||||
the span non-empty, so it is left alone entirely. This is what makes
|
||||
the gate precise: a sampler that deliberately supplies only its own
|
||||
attributes must not have them clobbered by a restore that assumes
|
||||
"no FastMCP keys" means "sampler forwarding failed."
|
||||
- A sampler that forwards most of our attributes but deliberately drops
|
||||
one is still non-empty, so it's covered by the same "leave alone"
|
||||
branch — a dropped key here is indistinguishable from the SDK's
|
||||
bounded attribute map evicting it, and reinserting it would just push
|
||||
the map's bound and evict a *different* retained key, churning which
|
||||
attributes survive without changing how many are lost. No attempt is
|
||||
made to restore individual missing keys; the gate is all-or-nothing.
|
||||
- A low `OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT` that evicts every attribute a
|
||||
forwarding sampler passed through is indistinguishable, from the
|
||||
span's attribute state alone, from a bare non-forwarding sampler —
|
||||
both leave an empty mapping. `dropped_attributes == 0` is what tells
|
||||
them apart: eviction always increments it, so that case is correctly
|
||||
excluded from the restore and the SDK's bounded map is left as
|
||||
computed.
|
||||
|
||||
Callers are expected to guard this with `if span.is_recording():`; it
|
||||
does no work worth skipping for non-recording spans, but the check is
|
||||
kept at call sites so it reads alongside the sibling `is_recording()`
|
||||
guards already in those functions.
|
||||
|
||||
|
||||
### `extract_trace_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L212" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
extract_trace_context(meta: dict[str, Any] | None) -> Context
|
||||
|
|
|
|||
92
docs/python-sdk/fastmcp-utilities-asgi_transport.mdx
Normal file
92
docs/python-sdk/fastmcp-utilities-asgi_transport.mdx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
---
|
||||
title: asgi_transport
|
||||
sidebarTitle: asgi_transport
|
||||
---
|
||||
|
||||
# `fastmcp.utilities.asgi_transport`
|
||||
|
||||
|
||||
An in-process, full-duplex HTTP transport for driving ASGI applications from httpx.
|
||||
|
||||
Ported from the MCP Python SDK's test suite (`tests/interaction/transports/_bridge.py`,
|
||||
MIT licensed).
|
||||
|
||||
`httpx2.ASGITransport` runs the application to completion and only then hands the buffered
|
||||
response to the caller, so a server that streams its response — as the streamable HTTP
|
||||
transport's SSE responses do — can never converse with the client mid-request: a
|
||||
server-initiated request nested inside a still-open call deadlocks.
|
||||
`StreamingASGITransport` removes that limitation by running the application as a background
|
||||
task and forwarding every `http.response.body` chunk to the client the moment it is sent.
|
||||
Everything happens on the one event loop: no sockets, no threads, no sleeps.
|
||||
|
||||
The behavioural contract:
|
||||
|
||||
- The request body is buffered before the application is invoked (MCP requests are small
|
||||
JSON documents); the response streams chunk by chunk.
|
||||
- Closing the response — or the whole client — delivers `http.disconnect` to the
|
||||
application, exactly as a real server sees when its peer goes away.
|
||||
- An exception the application raises before sending `http.response.start` fails the
|
||||
originating request with that same exception. After the response has started, a failure
|
||||
is visible to the client only through the response itself (status code, truncated body) —
|
||||
the same signal a real server over a real socket would give.
|
||||
|
||||
The transport owns an anyio task group for the application tasks; it is opened and closed by
|
||||
`httpx2.AsyncClient`'s own context manager, so the client must be used as a context manager.
|
||||
Closing the transport cancels every running application task by default; set
|
||||
`cancel_on_close=False` to wait for the application's own disconnect handling instead, which
|
||||
is what the legacy SSE transport relies on for resource cleanup.
|
||||
|
||||
|
||||
## Functions
|
||||
|
||||
### `run_asgi_lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/asgi_transport.py#L224" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
run_asgi_lifespan(app: ASGIApp) -> AsyncIterator[None]
|
||||
```
|
||||
|
||||
|
||||
Run an ASGI application's lifespan, driving the protocol as a real server does.
|
||||
|
||||
The application's lifespan runs inside a dedicated task for the whole duration of
|
||||
the context. This matters because a lifespan typically owns cancel scopes and task
|
||||
groups — anyio requires those to be exited by the task that entered them, which
|
||||
rules out entering the lifespan on one task and leaving it on another (as a pytest
|
||||
fixture's setup and teardown phases may do).
|
||||
|
||||
**Args:**
|
||||
- `app`: The ASGI application whose lifespan should run.
|
||||
|
||||
**Raises:**
|
||||
- `RuntimeError`: If the application reports `lifespan.startup.failed`, or reports
|
||||
`lifespan.shutdown.failed` (or crashes during shutdown) while the context
|
||||
body itself completed successfully. A failure inside the body takes
|
||||
precedence and propagates unchanged.
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `StreamingASGITransport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/asgi_transport.py#L71" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Drive an ASGI application in-process, streaming each response as it is produced.
|
||||
|
||||
This is an `httpx2` transport, so it plugs into anything that accepts an
|
||||
`httpx2.AsyncClient` — including FastMCP's client transports via their
|
||||
`httpx_client_factory` argument.
|
||||
|
||||
**Args:**
|
||||
- `app`: The ASGI application to drive (e.g. `FastMCP.http_app()`).
|
||||
- `cancel_on_close`: When True (the default), closing the transport cancels every
|
||||
application task still running, so harness teardown can never hang. Set to
|
||||
False to wait for the application's own disconnect handling to complete
|
||||
instead, which the legacy SSE server transport relies on for cleanup.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `handle_async_request` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/asgi_transport.py#L128" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
handle_async_request(self, request: httpx2.Request) -> httpx2.Response
|
||||
```
|
||||
|
|
@ -37,10 +37,10 @@ Uses anyio.to_thread.run_sync which properly propagates contextvars,
|
|||
making this safe for functions that depend on context (like dependency injection).
|
||||
|
||||
|
||||
### `gather` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/async_utils.py#L51" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `gather` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/async_utils.py#L53" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
gather(*awaitables: Awaitable[T]) -> list[T] | list[T | BaseException]
|
||||
gather(awaitables: Iterable[Awaitable[T]]) -> list[T] | list[T | BaseException]
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -48,8 +48,25 @@ Run awaitables concurrently and return results in order.
|
|||
|
||||
Uses anyio TaskGroup for structured concurrency.
|
||||
|
||||
``awaitables`` is consumed lazily, one item at a time, right before each
|
||||
is handed to the task group. Callers with a dynamic number of awaitables
|
||||
should pass a generator expression (e.g. ``gather(f(x) for x in xs)``)
|
||||
rather than a list or list comprehension: a list comprehension calls
|
||||
every ``f(x)`` up front, creating a batch of coroutine objects before
|
||||
this function even starts, whereas a generator expression creates each
|
||||
coroutine only as this function's own scheduling loop asks for it. That
|
||||
matters because coroutine creation and scheduling can be interrupted
|
||||
between any two bytecode instructions by a synchronous signal handler
|
||||
(for example pytest-timeout's SIGALRM-based per-test timeout). If that
|
||||
happens while a whole batch of coroutines is sitting unscheduled, they
|
||||
are silently abandoned and eventually trigger a "coroutine was never
|
||||
awaited" warning attributed to whatever unrelated code happens to be
|
||||
running when the garbage collector gets to them. Lazy consumption keeps
|
||||
the window in which a created-but-unscheduled coroutine can exist as
|
||||
small as possible.
|
||||
|
||||
**Args:**
|
||||
- `*awaitables`: Awaitables to run concurrently
|
||||
- `awaitables`: Iterable of awaitables to run concurrently.
|
||||
- `return_exceptions`: If True, exceptions are returned in results.
|
||||
If False, first exception cancels all and raises.
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ sidebarTitle: components
|
|||
|
||||
## Functions
|
||||
|
||||
### `get_fastmcp_metadata` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/components.py#L26" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `get_fastmcp_metadata` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/components.py#L22" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_fastmcp_metadata(meta: dict[str, Any] | None) -> FastMCPMeta
|
||||
|
|
@ -22,9 +22,9 @@ namespace for compatibility with older FastMCP servers.
|
|||
|
||||
## Classes
|
||||
|
||||
### `FastMCPMeta` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/components.py#L20" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `FastMCPMeta` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/components.py#L16" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
### `FastMCPComponent` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/components.py#L74" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `FastMCPComponent` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/components.py#L70" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Base class for FastMCP tools, prompts, resources, and resource templates.
|
||||
|
|
@ -114,53 +114,7 @@ copy(self) -> Self
|
|||
Create a copy of the component.
|
||||
|
||||
|
||||
#### `register_with_docket` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/components.py#L227" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
register_with_docket(self, docket: Docket) -> None
|
||||
```
|
||||
|
||||
Register this component with docket for background execution.
|
||||
|
||||
No-ops if task_config.mode is "forbidden". Subclasses override to
|
||||
register their callable (self.run, self.read, self.render, or self.fn).
|
||||
|
||||
|
||||
#### `coerce_task_arguments` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/components.py#L235" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
coerce_task_arguments(self, arguments: dict[str, Any]) -> dict[str, Any]
|
||||
```
|
||||
|
||||
Validate and coerce task arguments before any task state is created.
|
||||
|
||||
Called by ``submit_to_docket`` up front, so invalid inputs raise before
|
||||
the task's Redis metadata and initial status notification exist —
|
||||
otherwise a coercion failure during queueing would orphan a task the
|
||||
client has already observed. The base implementation is a no-op;
|
||||
components that splat arguments into a typed Python callable (e.g.
|
||||
``FunctionTool``) override this to mirror the synchronous validation
|
||||
path.
|
||||
|
||||
|
||||
#### `add_to_docket` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/components.py#L248" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_to_docket(self, docket: Docket, *args: Any, **kwargs: Any) -> Execution
|
||||
```
|
||||
|
||||
Schedule this component for background execution via docket.
|
||||
|
||||
Subclasses override this to handle their specific calling conventions:
|
||||
- Tool: add_to_docket(docket, arguments: dict, **kwargs)
|
||||
- Resource: add_to_docket(docket, **kwargs)
|
||||
- ResourceTemplate: add_to_docket(docket, params: dict, **kwargs)
|
||||
- Prompt: add_to_docket(docket, arguments: dict | None, **kwargs)
|
||||
|
||||
The **kwargs are passed through to docket.add() (e.g., key=task_key).
|
||||
|
||||
|
||||
#### `get_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/components.py#L270" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `get_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/components.py#L227" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_span_attributes(self) -> dict[str, Any]
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ sidebarTitle: exceptions
|
|||
|
||||
## Functions
|
||||
|
||||
### `iter_exc` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L12" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `iter_exc` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L36" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
iter_exc(group: BaseExceptionGroup)
|
||||
```
|
||||
|
||||
### `get_catch_handlers` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L42" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `get_catch_handlers` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L64" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_catch_handlers() -> Mapping[type[BaseException] | Iterable[type[BaseException]], Callable[[BaseExceptionGroup[Any]], Any]]
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ Extract information from a FastMCP v2.x instance.
|
|||
- FastMCPInfo dataclass containing the extracted information
|
||||
|
||||
|
||||
### `inspect_fastmcp_v1` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L236" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `inspect_fastmcp_v1` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L251" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo
|
||||
inspect_fastmcp_v1(mcp: SDKServer) -> FastMCPInfo
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -42,10 +42,10 @@ Extract information from a FastMCP v1.x instance using a Client.
|
|||
- FastMCPInfo dataclass containing the extracted information
|
||||
|
||||
|
||||
### `inspect_fastmcp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L378" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `inspect_fastmcp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L411" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
inspect_fastmcp(mcp: FastMCP[Any] | FastMCP1x) -> FastMCPInfo
|
||||
inspect_fastmcp(mcp: FastMCP[Any] | SDKServer) -> FastMCPInfo
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ and uses the appropriate extraction method.
|
|||
- FastMCPInfo dataclass containing the extracted information
|
||||
|
||||
|
||||
### `format_fastmcp_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L403" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `format_fastmcp_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L436" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
format_fastmcp_info(info: FastMCPInfo) -> bytes
|
||||
|
|
@ -73,10 +73,10 @@ Format FastMCPInfo as FastMCP-specific JSON.
|
|||
This includes FastMCP-specific fields like tags, enabled, annotations, etc.
|
||||
|
||||
|
||||
### `format_mcp_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L432" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `format_mcp_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L465" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
format_mcp_info(mcp: FastMCP[Any] | FastMCP1x) -> bytes
|
||||
format_mcp_info(mcp: FastMCP[Any] | SDKServer) -> bytes
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -86,10 +86,10 @@ Uses Client to get the standard MCP protocol format with camelCase fields.
|
|||
Includes version metadata at the top level.
|
||||
|
||||
|
||||
### `format_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L465" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `format_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L500" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
format_info(mcp: FastMCP[Any] | FastMCP1x, format: InspectFormat | Literal['fastmcp', 'mcp'], info: FastMCPInfo | None = None) -> bytes
|
||||
format_info(mcp: FastMCP[Any] | SDKServer, format: InspectFormat | Literal['fastmcp', 'mcp'], info: FastMCPInfo | None = None) -> bytes
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -136,7 +136,7 @@ Information about a resource template.
|
|||
Information extracted from a FastMCP instance.
|
||||
|
||||
|
||||
### `InspectFormat` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L396" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `InspectFormat` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L429" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Output format for inspect command.
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ the referenced definition while preserving $defs for nested references.
|
|||
- if no resolution is needed
|
||||
|
||||
|
||||
### `compress_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L688" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `compress_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L693" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
compress_schema(schema: dict[str, Any], prune_params: list[str] | None = None, prune_additional_properties: bool = False, prune_titles: bool = False, dereference: bool = False) -> dict[str, Any]
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Task configuration primitives for FastMCP components.
|
|||
|
||||
## Classes
|
||||
|
||||
### `TaskMeta` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tasks.py#L22" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `TaskMeta` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tasks.py#L29" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Metadata for task-augmented execution requests.
|
||||
|
|
@ -20,7 +20,7 @@ Metadata for task-augmented execution requests.
|
|||
- `fn_key`: Docket routing key. Auto-derived from component name if None.
|
||||
|
||||
|
||||
### `TaskConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tasks.py#L35" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `TaskConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tasks.py#L42" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Configuration for MCP background task execution.
|
||||
|
|
@ -34,7 +34,7 @@ Controls how a component handles task-augmented requests:
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `from_bool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tasks.py#L49" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `from_bool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tasks.py#L56" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
from_bool(cls, value: bool) -> TaskConfig
|
||||
|
|
@ -43,7 +43,7 @@ from_bool(cls, value: bool) -> TaskConfig
|
|||
Convert a boolean task flag to a TaskConfig.
|
||||
|
||||
|
||||
#### `supports_tasks` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tasks.py#L53" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `supports_tasks` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tasks.py#L60" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
supports_tasks(self) -> bool
|
||||
|
|
@ -52,7 +52,7 @@ supports_tasks(self) -> bool
|
|||
Check if this component supports task execution.
|
||||
|
||||
|
||||
#### `validate_function` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tasks.py#L57" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `validate_function` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tasks.py#L64" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
validate_function(self, fn: Callable[..., Any], name: str) -> None
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ sidebarTitle: tests
|
|||
|
||||
## Functions
|
||||
|
||||
### `temporary_settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L24" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `temporary_settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L36" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
temporary_settings(**kwargs: Any)
|
||||
|
|
@ -20,7 +20,7 @@ Temporarily override FastMCP setting values.
|
|||
- `**kwargs`: The settings to override, including nested settings.
|
||||
|
||||
|
||||
### `run_server_in_process` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L75" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `run_server_in_process` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L87" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
run_server_in_process(server_fn: Callable[..., None], *args: Any, **kwargs: Any) -> Generator[str, None, None]
|
||||
|
|
@ -43,18 +43,20 @@ not pickleable, so we need a function that creates and runs one.
|
|||
- The server URL.
|
||||
|
||||
|
||||
### `run_server_async` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L143" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `run_server_async` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L175" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
run_server_async(server: FastMCP, port: int | None = None, transport: Literal['http', 'streamable-http', 'sse'] = 'http', path: str = '/mcp', host: str = '127.0.0.1') -> AsyncGenerator[str, None]
|
||||
```
|
||||
|
||||
|
||||
Start a FastMCP server as an asyncio task for in-process async testing.
|
||||
Start a FastMCP server on a real port as an asyncio task.
|
||||
|
||||
This is the recommended way to test FastMCP servers. It runs the server
|
||||
as an async task in the same process, eliminating subprocess coordination,
|
||||
sleeps, and cleanup issues.
|
||||
This runs a real uvicorn server in the current process, bound to a real TCP port,
|
||||
and yields its URL. Use it when the behaviour under test is genuinely about the
|
||||
network — real sockets, TLS, or a server that must be reachable by something other
|
||||
than an in-process client. Otherwise prefer `asgi_client` or `asgi_server`, which
|
||||
exercise the same HTTP stack without binding a port.
|
||||
|
||||
**Args:**
|
||||
- `server`: FastMCP server instance
|
||||
|
|
@ -64,9 +66,124 @@ sleeps, and cleanup issues.
|
|||
- `host`: Host to bind to (default\: "127.0.0.1")
|
||||
|
||||
|
||||
### `asgi_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L335" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
asgi_server(server: FastMCP, transport: Literal['http', 'streamable-http', 'sse'] = 'http', path: str | None = None, **http_app_kwargs: Any) -> AsyncGenerator[ASGIServer, None]
|
||||
```
|
||||
|
||||
|
||||
Serve a FastMCP server's HTTP app in-process, with no socket and no uvicorn.
|
||||
|
||||
This is the fastest way to test a FastMCP server over HTTP. The server's real
|
||||
Starlette app is built with `http_app()` and its lifespan is started, then every
|
||||
request is dispatched directly into the app on the current event loop. That skips
|
||||
port binding, uvicorn startup and connection setup entirely, while still exercising
|
||||
the full HTTP stack: middleware, authentication, session management and SSE
|
||||
streaming all run exactly as they do in production.
|
||||
|
||||
Use this as a fixture when several tests share one server but each needs its own
|
||||
client. For a single test, `asgi_client` hands you a connected client in one step.
|
||||
|
||||
**Args:**
|
||||
- `server`: FastMCP server instance.
|
||||
- `transport`: Transport type ("http", "streamable-http", or "sse").
|
||||
- `path`: URL path for the server (defaults to "/mcp", or "/sse" for SSE).
|
||||
- `**http_app_kwargs`: Additional arguments forwarded to `server.http_app()`.
|
||||
|
||||
|
||||
### `asgi_client` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L409" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
asgi_client(server: FastMCP, transport: Literal['http', 'streamable-http', 'sse'] = 'http', path: str | None = None, **client_kwargs: Any) -> AsyncGenerator[Client, None]
|
||||
```
|
||||
|
||||
|
||||
Serve a FastMCP server over HTTP in-process and yield a connected `Client`.
|
||||
|
||||
This is the shortest path to testing a server over a real HTTP stack. The server's
|
||||
Starlette app is built and started, and requests are dispatched straight into it on
|
||||
the current event loop — no port, no uvicorn, no subprocess — but middleware,
|
||||
authentication, session management and SSE streaming all behave as in production.
|
||||
|
||||
Reach for `asgi_server` instead when a fixture must serve several tests that each
|
||||
build their own client, or when a test needs raw HTTP access to the app.
|
||||
|
||||
**Args:**
|
||||
- `server`: FastMCP server instance.
|
||||
- `transport`: Transport type ("http", "streamable-http", or "sse").
|
||||
- `path`: URL path for the server (defaults to "/mcp", or "/sse" for SSE).
|
||||
- `headers`: HTTP headers to send with every request.
|
||||
- `auth`: Client authentication, as accepted by the HTTP transports.
|
||||
- `**client_kwargs`: Additional arguments forwarded to `Client`.
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `HeadlessOAuth` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L225" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ASGIServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L256" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A FastMCP server's real HTTP app, reachable in-process with no sockets.
|
||||
|
||||
Yielded by `asgi_server`. The `url` looks like an ordinary server URL and the app
|
||||
behind it is the genuine article — auth middleware, session manager, SSE framing and
|
||||
redirects all run — but every request is dispatched straight into the ASGI
|
||||
application on the current event loop.
|
||||
|
||||
Because nothing is listening on the network, a plain `httpx2.AsyncClient()` cannot
|
||||
reach this server. Use `client()` for a FastMCP client, `http_client()` for raw HTTP
|
||||
assertions, and `transport()` when you need to build the client transport yourself.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `http_client` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L273" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
http_client(self, headers: dict[str, str] | None = None, timeout: httpx2.Timeout | None = None, auth: httpx2.Auth | None = None, **kwargs: Any) -> httpx2.AsyncClient
|
||||
```
|
||||
|
||||
An `httpx2.AsyncClient` bound to the in-process app, for raw HTTP assertions.
|
||||
|
||||
Relative URLs resolve against the server's base URL, and absolute URLs on the
|
||||
same origin work too, so `client.get(f"{server.url}/health")` reads the same as
|
||||
it would against a real server.
|
||||
|
||||
The signature matches `McpHttpClientFactory`, so this method can also be handed
|
||||
to anything that takes an `httpx_client_factory`.
|
||||
|
||||
|
||||
#### `transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L302" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
transport(self, **kwargs: Any) -> StreamableHttpTransport | SSETransport
|
||||
```
|
||||
|
||||
A FastMCP client transport wired to the in-process app.
|
||||
|
||||
Accepts the same keyword arguments as the underlying transport (`headers`,
|
||||
`auth`, ...); `httpx_client_factory` is supplied automatically.
|
||||
|
||||
|
||||
#### `client` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L313" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
client(self, **client_kwargs: Any) -> Client
|
||||
```
|
||||
|
||||
An unconnected FastMCP `Client` pointed at the in-process app.
|
||||
|
||||
`headers` and `auth` configure the underlying HTTP transport; every other
|
||||
keyword argument is passed to `Client` (`timeout`, `elicitation_handler`, ...).
|
||||
Use it as a context manager, exactly like any other client.
|
||||
|
||||
**Args:**
|
||||
- `headers`: HTTP headers to send with every request.
|
||||
- `auth`: Client authentication, as accepted by the HTTP transports.
|
||||
- `**client_kwargs`: Additional arguments forwarded to `Client`.
|
||||
|
||||
|
||||
### `HeadlessOAuth` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L464" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
OAuth provider that bypasses browser interaction for testing.
|
||||
|
|
@ -77,7 +194,7 @@ instead of opening a browser and running a callback server. Useful for automated
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `redirect_handler` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L238" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `redirect_handler` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L477" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
redirect_handler(self, authorization_url: str) -> None
|
||||
|
|
@ -86,11 +203,11 @@ redirect_handler(self, authorization_url: str) -> None
|
|||
Make HTTP request to authorization URL and store response for callback handler.
|
||||
|
||||
|
||||
#### `callback_handler` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L244" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `callback_handler` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/tests.py#L483" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
callback_handler(self) -> tuple[str, str | None]
|
||||
callback_handler(self) -> AuthorizationCodeResult
|
||||
```
|
||||
|
||||
Parse stored response and return (auth_code, state).
|
||||
Parse stored response and return the authorization code result.
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue