Merge origin/main into feature/apps

This commit is contained in:
Jeremiah Lowin 2026-02-13 20:41:53 -05:00
commit ccb74da45c
No known key found for this signature in database
162 changed files with 8818 additions and 2836 deletions

95
.github/actions/run-claude/action.yml vendored Normal file
View file

@ -0,0 +1,95 @@
# Composite Action for running Claude Code Action
#
# Wraps anthropics/claude-code-action with MCP server configuration.
# Template based on elastic/ai-github-actions base action.
#
# Usage:
# - uses: ./.github/actions/run-claude
# with:
# prompt: "Your prompt here"
# claude-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# github-token: ${{ steps.marvin-token.outputs.token }}
# allowed-tools: "Edit,Read,Write,Bash(*),mcp__github__add_issue_comment"
#
name: "Run Claude"
description: "Run Claude Code with MCP servers"
author: "FastMCP"
branding:
icon: "cpu"
color: "orange"
inputs:
prompt:
description: "Prompt to pass to Claude"
required: true
claude-oauth-token:
description: "Claude Code OAuth token for authentication"
required: true
github-token:
description: "GitHub token for Claude to operate with"
required: true
allowed-tools:
description: "Comma-separated list of allowed tools (e.g. Edit,Write,Bash(npm test))"
required: false
default: ""
model:
description: "Model to use for Claude"
required: false
default: "claude-opus-4-6"
allowed-bots:
description: "Allowed bot usernames, or '*' for all bots"
required: false
default: ""
track-progress:
description: "Whether Claude should track progress"
required: false
default: "true"
mcp-servers:
description: "MCP server configuration JSON"
required: false
default: '{"mcpServers":{"agents-md-generator":{"type":"http","url":"https://agents-md-generator.fastmcp.app/mcp"},"public-code-search":{"type":"http","url":"https://public-code-search.fastmcp.app/mcp"}}}'
trigger-phrase:
description: "Trigger phrase (for mention workflows)"
required: false
default: "/marvin"
outputs:
conclusion:
description: "The conclusion of the Claude Code run"
value: ${{ steps.claude.outputs.conclusion }}
runs:
using: "composite"
steps:
- name: Clean up stale Claude locks
shell: bash
run: rm -rf ~/.claude/.locks ~/.local/state/claude/locks || true
- name: Run Claude Code
id: claude
env:
GITHUB_TOKEN: ${{ inputs.github-token }}
uses: anthropics/claude-code-action@v1
with:
github_token: ${{ inputs.github-token }}
claude_code_oauth_token: ${{ inputs.claude-oauth-token }}
bot_name: "Marvin Context Protocol"
trigger_phrase: ${{ inputs.trigger-phrase }}
allowed_bots: ${{ inputs.allowed-bots }}
track_progress: ${{ inputs.track-progress }}
prompt: ${{ inputs.prompt }}
claude_args: |
${{ (inputs.allowed-tools != '' || inputs.extra-allowed-tools != '') && format('--allowedTools {0}{1}', inputs.allowed-tools, inputs.extra-allowed-tools != '' && format(',{0}', inputs.extra-allowed-tools) || '') || '' }}
${{ inputs.mcp-servers != '' && format('--mcp-config ''{0}''', inputs.mcp-servers) || '' }}
--model ${{ inputs.model }}
settings: |
{"model": "${{ inputs.model }}"}

View file

@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -euo pipefail
# Get PR review threads with comments via GitHub GraphQL API
#
# Usage:
# gh-get-review-threads.sh [FILTER]
#
# Arguments:
# FILTER - Optional: filter for unresolved threads from specific author
#
# Environment (set by composite action):
# MENTION_REPO - Repository (owner/repo format)
# MENTION_PR_NUMBER - Pull request number
# GITHUB_TOKEN - GitHub API token
#
# Output:
# JSON array of review threads with nested comments
# Parse OWNER and REPO from MENTION_REPO
REPO_FULL="${MENTION_REPO:?MENTION_REPO environment variable is required}"
OWNER="${REPO_FULL%/*}"
REPO="${REPO_FULL#*/}"
PR_NUMBER="${MENTION_PR_NUMBER:?MENTION_PR_NUMBER environment variable is required}"
FILTER="${1:-}"
gh api graphql -f query='
query($owner: String!, $repo: String!, $prNumber: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
reviewThreads(first: 100) {
nodes {
id
isResolved
isOutdated
path
line
comments(first: 50) {
nodes {
id
body
author { login }
createdAt
}
}
}
}
}
}
}' -F owner="$OWNER" \
-F repo="$REPO" \
-F prNumber="$PR_NUMBER" \
--jq '.data.repository.pullRequest.reviewThreads.nodes' | \
if [ -n "$FILTER" ]; then
jq --arg author "$FILTER" '
map(select(
.isResolved == false and
.comments.nodes | any(.author.login == $author)
))'
else
cat
fi

View file

@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -euo pipefail
# Resolve a GitHub PR review thread, optionally posting a comment first
#
# Usage:
# gh-resolve-review-thread.sh THREAD_ID [COMMENT]
#
# Arguments:
# THREAD_ID - The GraphQL node ID of the review thread to resolve
# COMMENT - Optional: Comment body to post before resolving
#
# Environment (set by composite action):
# MENTION_REPO - Repository (owner/repo format)
# MENTION_PR_NUMBER - Pull request number
# GITHUB_TOKEN - GitHub API token
#
# Behavior:
# 1. If COMMENT is provided, posts it as a reply to the thread
# 2. Resolves the thread
# Validate required environment variables
: "${MENTION_REPO:?MENTION_REPO environment variable is required}"
: "${MENTION_PR_NUMBER:?MENTION_PR_NUMBER environment variable is required}"
THREAD_ID="${1:?Thread ID required}"
COMMENT="${2:-}"
# Step 1: Post comment if provided
if [ -n "$COMMENT" ]; then
echo "Posting comment to thread..." >&2
COMMENT_RESULT=$(gh api graphql -f query='
mutation($threadId: ID!, $body: String!) {
addPullRequestReviewThreadReply(input: {
pullRequestReviewThreadId: $threadId,
body: $body
}) {
comment {
id
}
}
}' -f threadId="$THREAD_ID" -f body="$COMMENT")
if echo "$COMMENT_RESULT" | jq -e '.errors' > /dev/null 2>&1; then
echo "Error posting comment: $COMMENT_RESULT" >&2
exit 1
fi
fi
# Step 2: Resolve the thread
echo "Resolving thread..." >&2
RESOLVE_RESULT=$(gh api graphql -f query='
mutation($threadId: ID!) {
resolveReviewThread(input: {threadId: $threadId}) {
thread {
id
isResolved
}
}
}' -f threadId="$THREAD_ID" --jq '.data.resolveReviewThread.thread')
echo "$RESOLVE_RESULT"
echo "✓ Thread resolved" >&2

251
.github/scripts/pr-review/pr-comment.sh vendored Executable file
View file

@ -0,0 +1,251 @@
#!/bin/bash
# pr-comment.sh - Queue a structured inline review comment for the PR review
#
# Usage:
# pr-comment.sh <file> <line> --severity <level> --title <description> --why <reason> [suggestion via stdin]
# pr-comment.sh <file> <line> --severity <level> --title <description> --why <reason> --no-suggestion
#
# Arguments:
# file File path (required)
# line Line number (required)
# --severity Severity level: critical, high, medium, low, nitpick (required)
# --title Brief description for comment heading (required)
# --why One sentence explaining the risk/impact (required)
# --no-suggestion Explicitly skip suggestion (use for architectural issues)
#
# The suggestion code is read from stdin (use heredoc). If no stdin and no --no-suggestion, errors.
#
# Examples:
# # With suggestion (preferred)
# pr-comment.sh src/main.go 42 --severity high --title "Missing error check" --why "Errors are silently ignored" <<'EOF'
# if err != nil {
# return fmt.Errorf("operation failed: %w", err)
# }
# EOF
#
# # Without suggestion (for issues requiring broader changes)
# pr-comment.sh src/main.go 42 --severity medium --title "Consider extracting to function" \
# --why "This logic is duplicated in 3 places" --no-suggestion
#
# Environment variables (set by the composite action):
# PR_REVIEW_REPO - Repository (owner/repo)
# PR_REVIEW_PR_NUMBER - Pull request number
# PR_REVIEW_COMMENTS_DIR - Directory to cache comments (default: /tmp/pr-review-comments)
set -e
# Configuration from environment
REPO="${PR_REVIEW_REPO:?PR_REVIEW_REPO environment variable is required}"
PR_NUMBER="${PR_REVIEW_PR_NUMBER:?PR_REVIEW_PR_NUMBER environment variable is required}"
COMMENTS_DIR="${PR_REVIEW_COMMENTS_DIR:-/tmp/pr-review-comments}"
# Severity emoji mapping
declare -A SEVERITY_EMOJI=(
[critical]="🔴 CRITICAL"
[high]="🟠 HIGH"
[medium]="🟡 MEDIUM"
[low]="⚪ LOW"
[nitpick]="💬 NITPICK"
)
# Parse arguments
FILE=""
LINE=""
SEVERITY=""
TITLE=""
WHY=""
NO_SUGGESTION=false
# First two positional args are file and line
if [ $# -lt 2 ]; then
echo "Error: file and line are required"
echo "Usage: pr-comment.sh <file> <line> --severity <level> --title <desc> --why <reason> [<<'EOF' ... EOF]"
exit 1
fi
FILE="$1"
LINE="$2"
shift 2
# Parse named arguments
while [ $# -gt 0 ]; do
case "$1" in
--severity)
SEVERITY="$2"
shift 2
;;
--title)
TITLE="$2"
shift 2
;;
--why)
WHY="$2"
shift 2
;;
--no-suggestion)
NO_SUGGESTION=true
shift
;;
*)
echo "Error: Unknown argument: $1"
exit 1
;;
esac
done
# Read suggestion from stdin if available
SUGGESTION=""
if [ ! -t 0 ]; then
SUGGESTION=$(cat)
fi
# Validate required arguments
if [ -z "$SEVERITY" ]; then
echo "Error: --severity is required (critical, high, medium, low, nitpick)"
exit 1
fi
if [ -z "$TITLE" ]; then
echo "Error: --title is required"
exit 1
fi
if [ -z "$WHY" ]; then
echo "Error: --why is required"
exit 1
fi
# Validate severity level
if [ -z "${SEVERITY_EMOJI[$SEVERITY]}" ]; then
echo "Error: Invalid severity '$SEVERITY'. Must be one of: critical, high, medium, low, nitpick"
exit 1
fi
# Require either suggestion or explicit --no-suggestion
if [ -z "$SUGGESTION" ] && [ "$NO_SUGGESTION" = false ]; then
echo "Error: Suggestion required. Provide code via stdin (heredoc) or use --no-suggestion"
echo ""
echo "Example with suggestion:"
echo " pr-comment.sh file.go 42 --severity high --title \"desc\" --why \"reason\" <<'EOF'"
echo " fixed code here"
echo " EOF"
echo ""
echo "Example without suggestion:"
echo " pr-comment.sh file.go 42 --severity medium --title \"desc\" --why \"reason\" --no-suggestion"
exit 1
fi
# Validate line is a positive integer (>= 1)
if ! [[ "$LINE" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: Line number must be a positive integer (>= 1), got: $LINE"
exit 1
fi
# Get the diff for this file to validate the comment location
DIFF_DATA=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate | jq --arg f "$FILE" '.[] | select(.filename==$f)')
if [ -z "$DIFF_DATA" ]; then
echo "Error: File '${FILE}' not found in PR diff"
echo ""
echo "Files changed in this PR:"
gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate --jq '.[].filename'
exit 1
fi
PATCH=$(echo "$DIFF_DATA" | jq -r '.patch // empty')
if [ -z "$PATCH" ]; then
echo "Error: No patch data for file '${FILE}' (file may be binary or too large)"
exit 1
fi
# Verify the line exists in the diff
LINE_IN_DIFF=$(echo "$PATCH" | awk -v target_line="$LINE" '
BEGIN { current_line = 0; found = 0 }
/^@@/ {
line = $0
gsub(/.*\+/, "", line)
gsub(/[^0-9].*/, "", line)
current_line = line - 1
next
}
{
if (substr($0, 1, 1) != "-") {
current_line++
if (current_line == target_line) {
found = 1
exit
}
}
}
END { if (found) print "1"; else print "0" }
')
if [ "$LINE_IN_DIFF" != "1" ]; then
echo "Error: Line ${LINE} not found in the diff for '${FILE}'"
echo ""
echo "Note: You can only comment on lines that appear in the diff (added, modified, or context lines)"
echo ""
echo "First 50 lines of diff for this file:"
echo "$PATCH" | head -50
exit 1
fi
# Create comments directory if it doesn't exist
mkdir -p "${COMMENTS_DIR}"
# Assemble the comment body
SEVERITY_LABEL="${SEVERITY_EMOJI[$SEVERITY]}"
BODY="**${SEVERITY_LABEL}** ${TITLE}
Why: ${WHY}"
# Add suggestion block if provided
if [ -n "$SUGGESTION" ]; then
BODY="${BODY}
\`\`\`suggestion
${SUGGESTION}
\`\`\`"
fi
# Append standard footer
FOOTER='
---
Marvin Context Protocol | Type `/marvin` to interact further
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.'
BODY_WITH_FOOTER="${BODY}${FOOTER}"
# Generate unique comment ID
COMMENT_ID="comment-$(date +%s)-$(od -An -N4 -tu4 /dev/urandom | tr -d ' ')"
COMMENT_FILE="${COMMENTS_DIR}/${COMMENT_ID}.json"
# Create the comment JSON object
jq -n \
--arg path "$FILE" \
--argjson line "$LINE" \
--arg side "RIGHT" \
--arg body "$BODY_WITH_FOOTER" \
--arg id "$COMMENT_ID" \
'{
path: $path,
line: $line,
side: $side,
body: $body,
_meta: {
id: $id,
file: $path,
line: $line
}
}' > "${COMMENT_FILE}"
echo "✓ Queued review comment for ${FILE}:${LINE}"
echo " Severity: ${SEVERITY_LABEL}"
echo " Title: ${TITLE}"
echo " Comment ID: ${COMMENT_ID}"
echo " Comment will be submitted with pr-review.sh"
echo " Remove with: pr-remove-comment.sh ${FILE} ${LINE}"

128
.github/scripts/pr-review/pr-diff.sh vendored Executable file
View file

@ -0,0 +1,128 @@
#!/bin/bash
# pr-diff.sh - Show changed files or diff for a specific file
#
# Usage:
# pr-diff.sh - List all changed files (shows full diff if small enough)
# pr-diff.sh <file> - Show diff for a specific file with line numbers
#
# Environment variables (set by the composite action):
# PR_REVIEW_REPO - Repository (owner/repo)
# PR_REVIEW_PR_NUMBER - Pull request number
set -e
# Configuration from environment
REPO="${PR_REVIEW_REPO:?PR_REVIEW_REPO environment variable is required}"
PR_NUMBER="${PR_REVIEW_PR_NUMBER:?PR_REVIEW_PR_NUMBER environment variable is required}"
EXPECTED_HEAD="${PR_REVIEW_HEAD_SHA:-}"
# Check if HEAD has changed since review started (race condition detection)
if [ -n "$EXPECTED_HEAD" ]; then
CURRENT_HEAD=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha')
if [ "$CURRENT_HEAD" != "$EXPECTED_HEAD" ]; then
echo "⚠️ WARNING: PR head has changed since review started!"
echo " Review started at: ${EXPECTED_HEAD:0:7}"
echo " Current head: ${CURRENT_HEAD:0:7}"
echo " Line numbers below may not match the commit being reviewed."
echo ""
fi
fi
# Thresholds for "too big" - show file list only if exceeded
MAX_FILES=25
MAX_TOTAL_LINES=1500
FILE="$1"
# Function to add line numbers to a patch
# Format: [LINE] +added | [LINE] context | [----] -deleted
add_line_numbers() {
awk '
BEGIN { new_line = 0 }
/^@@/ {
# Parse hunk header: @@ -old_start,old_count +new_start,new_count @@
match($0, /\+([0-9]+)/)
new_line = substr($0, RSTART+1, RLENGTH-1) - 1
print ""
print $0
next
}
/^-/ {
# Deleted line - cannot comment on these
printf "[----] %s\n", $0
next
}
/^\+/ {
# Added line - can comment, show line number
new_line++
printf "[%4d] %s\n", new_line, $0
next
}
{
# Context line (space prefix) - can comment, show line number
new_line++
printf "[%4d] %s\n", new_line, $0
}
'
}
if [ -z "$FILE" ]; then
# Get file list with stats
FILES_DATA=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate)
FILE_COUNT=$(echo "$FILES_DATA" | jq 'length')
TOTAL_ADDITIONS=$(echo "$FILES_DATA" | jq '[.[].additions] | add // 0')
TOTAL_DELETIONS=$(echo "$FILES_DATA" | jq '[.[].deletions] | add // 0')
TOTAL_LINES=$((TOTAL_ADDITIONS + TOTAL_DELETIONS))
echo "PR #${PR_NUMBER} Summary: ${FILE_COUNT} files changed (+${TOTAL_ADDITIONS}/-${TOTAL_DELETIONS})"
echo ""
# Check if diff is too large
if [ "$FILE_COUNT" -gt "$MAX_FILES" ] || [ "$TOTAL_LINES" -gt "$MAX_TOTAL_LINES" ]; then
echo "⚠️ Large diff detected (>${MAX_FILES} files or >${MAX_TOTAL_LINES} lines changed)"
echo " Review files individually using: pr-diff.sh <filename>"
echo ""
echo "Files changed:"
echo "$FILES_DATA" | jq -r '.[] | " \(.filename) (+\(.additions)/-\(.deletions))"'
else
# Small enough - show all diffs with line numbers
echo "Files changed:"
echo "$FILES_DATA" | jq -r '.[] | " \(.filename) (+\(.additions)/-\(.deletions))"'
echo ""
echo "─────────────────────────────────────────────────────────────────────"
echo ""
# Show each file's diff by iterating over indices
for i in $(seq 0 $((FILE_COUNT - 1))); do
FNAME=$(echo "$FILES_DATA" | jq -r ".[$i].filename")
PATCH=$(echo "$FILES_DATA" | jq -r ".[$i].patch // empty")
if [ -n "$PATCH" ]; then
echo "## ${FNAME}"
echo "Use: pr-comment.sh ${FNAME} <LINE> --severity <level> --title \"desc\" --why \"reason\" <<'EOF' ... EOF"
echo "Format: [LINE] +added | [LINE] context | [----] -deleted (can't comment)"
echo "$PATCH" | add_line_numbers
echo ""
echo "─────────────────────────────────────────────────────────────────────"
echo ""
fi
done
fi
else
# Show specific file diff
PATCH=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate --jq --arg file "$FILE" '.[] | select(.filename==$file) | .patch')
if [ -z "$PATCH" ]; then
echo "Error: File '${FILE}' not found in PR diff"
echo ""
echo "Files changed in this PR:"
gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate --jq '.[].filename'
exit 1
fi
echo "## ${FILE}"
echo "Use: pr-comment.sh ${FILE} <LINE> --severity <level> --title \"desc\" --why \"reason\" <<'EOF' ... EOF"
echo "Format: [LINE] +added | [LINE] context | [----] -deleted (can't comment)"
echo "$PATCH" | add_line_numbers
fi

View file

@ -0,0 +1,190 @@
#!/bin/bash
# pr-existing-comments.sh - Fetch existing review threads on a PR
#
# Usage:
# pr-existing-comments.sh - Show all review threads with full details
# pr-existing-comments.sh --summary - Show per-file summary only (for large PRs)
# pr-existing-comments.sh --unresolved - Show only unresolved threads
# pr-existing-comments.sh --file <path> - Show threads for a specific file
# pr-existing-comments.sh --full - Show full comment text (no truncation)
#
# Output: Formatted summary of existing review threads grouped by file,
# showing thread status, comments, and whether issues were addressed.
#
# For large PRs, use --summary first to see the overview, then --file <path>
# to get full thread details when reviewing each file.
#
# Environment variables (set by the composite action):
# PR_REVIEW_REPO - Repository (owner/repo)
# PR_REVIEW_PR_NUMBER - Pull request number
set -e
# Configuration from environment
REPO="${PR_REVIEW_REPO:?PR_REVIEW_REPO environment variable is required}"
PR_NUMBER="${PR_REVIEW_PR_NUMBER:?PR_REVIEW_PR_NUMBER environment variable is required}"
OWNER="${REPO%/*}"
REPO_NAME="${REPO#*/}"
# Parse arguments
FILTER_UNRESOLVED=false
FILTER_FILE=""
SUMMARY_ONLY=false
FULL_TEXT=false
while [ $# -gt 0 ]; do
case "$1" in
--unresolved)
FILTER_UNRESOLVED=true
shift
;;
--file)
FILTER_FILE="$2"
shift 2
;;
--summary)
SUMMARY_ONLY=true
shift
;;
--full)
FULL_TEXT=true
shift
;;
*)
echo "Usage: pr-existing-comments.sh [--summary] [--unresolved] [--file <path>] [--full]"
exit 1
;;
esac
done
# Fetch review threads via GraphQL
THREADS=$(gh api graphql -f query='
query($owner: String!, $repo: String!, $prNumber: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
reviewThreads(first: 100) {
nodes {
id
isResolved
isOutdated
path
line
originalLine
startLine
originalStartLine
diffSide
comments(first: 50) {
nodes {
id
body
author { login }
createdAt
originalCommit { abbreviatedOid }
}
}
}
}
}
}
}' -F owner="$OWNER" \
-F repo="$REPO_NAME" \
-F prNumber="$PR_NUMBER" \
--jq '.data.repository.pullRequest.reviewThreads.nodes')
if [ -z "$THREADS" ] || [ "$THREADS" = "null" ]; then
echo "No existing review threads found."
exit 0
fi
# Apply filters
FILTERED="$THREADS"
if [ "$FILTER_UNRESOLVED" = true ]; then
FILTERED=$(echo "$FILTERED" | jq '[.[] | select(.isResolved == false)]')
fi
if [ -n "$FILTER_FILE" ]; then
FILTERED=$(echo "$FILTERED" | jq --arg file "$FILTER_FILE" '[.[] | select(.path == $file)]')
fi
THREAD_COUNT=$(echo "$FILTERED" | jq 'length')
if [ "$THREAD_COUNT" -eq 0 ]; then
if [ "$FILTER_UNRESOLVED" = true ]; then
echo "No unresolved review threads found."
elif [ -n "$FILTER_FILE" ]; then
echo "No review threads found for ${FILTER_FILE}."
else
echo "No existing review threads found."
fi
exit 0
fi
# Count resolved vs unresolved
RESOLVED_COUNT=$(echo "$FILTERED" | jq '[.[] | select(.isResolved == true)] | length')
UNRESOLVED_COUNT=$(echo "$FILTERED" | jq '[.[] | select(.isResolved == false)] | length')
OUTDATED_COUNT=$(echo "$FILTERED" | jq '[.[] | select(.isOutdated == true)] | length')
echo "Existing review threads: ${THREAD_COUNT} total (${UNRESOLVED_COUNT} unresolved, ${RESOLVED_COUNT} resolved, ${OUTDATED_COUNT} outdated)"
echo ""
# Summary mode: show per-file counts only
if [ "$SUMMARY_ONLY" = true ]; then
echo "Threads by file:"
echo "$FILTERED" | jq -r '
group_by(.path) | .[] |
. as $threads |
($threads | length) as $total |
([$threads[] | select(.isResolved == false)] | length) as $unresolved |
([$threads[] | select(.isResolved == true)] | length) as $resolved |
([$threads[] | select(.isOutdated == true)] | length) as $outdated |
([$threads[] | select(.comments.nodes | length > 1)] | length) as $has_replies |
" " + $threads[0].path +
" — " + ($total | tostring) + " threads" +
" (" + ($unresolved | tostring) + " unresolved, " + ($resolved | tostring) + " resolved" +
(if $outdated > 0 then ", " + ($outdated | tostring) + " outdated" else "" end) +
")" +
(if $has_replies > 0 then " ⚠️ " + ($has_replies | tostring) + " with replies" else "" end)
'
echo ""
echo "Use: pr-existing-comments.sh --file <path> to see full thread details for a file"
exit 0
fi
# Full detail mode: output threads grouped by file
# Show full conversation for threads with replies
FIRST_LIMIT=200
REPLY_LIMIT=300
if [ "$FULL_TEXT" = true ]; then
FIRST_LIMIT=999999
REPLY_LIMIT=999999
fi
echo "$FILTERED" | jq -r --argjson first_limit "$FIRST_LIMIT" --argjson reply_limit "$REPLY_LIMIT" '
group_by(.path) | .[] |
"## " + .[0].path + " (" + (length | tostring) + " threads)\n" +
([.[] |
" " +
(if .isResolved then "✅ RESOLVED" elif .isOutdated then "⚠️ OUTDATED" else "🔴 UNRESOLVED" end) +
" (line " + (if .line then (.line | tostring) elif .startLine then (.startLine | tostring) elif .originalLine then ("~" + (.originalLine | tostring)) elif .originalStartLine then ("~" + (.originalStartLine | tostring)) else "?" end) + ")" +
# Show the commit the comment was originally made on
(if .comments.nodes[0].originalCommit.abbreviatedOid then " [" + .comments.nodes[0].originalCommit.abbreviatedOid + "]" else "" end) +
# Flag threads with replies — indicates a conversation happened
(if (.comments.nodes | length) > 1 then " ← has replies" else "" end) +
"\n" +
([.comments.nodes | to_entries[] |
.value as $comment |
.key as $idx |
($comment.body | gsub("\n"; " ")) as $flat |
if $idx == 0 then
" @" + ($comment.author.login // "unknown") + ": " + $flat[0:$first_limit] +
(if ($flat | length) > $first_limit then " [truncated]" else "" end)
else
" ↳ @" + ($comment.author.login // "unknown") + ": " + $flat[0:$reply_limit] +
(if ($flat | length) > $reply_limit then " [truncated]" else "" end)
end
] | join("\n")) +
"\n"
] | join("\n"))
'

View file

@ -0,0 +1,84 @@
#!/bin/bash
# pr-remove-comment.sh - Remove a queued review comment
#
# Usage:
# pr-remove-comment.sh <file> <line-number>
# pr-remove-comment.sh <comment-id>
#
# Examples:
# pr-remove-comment.sh src/main.go 42
# pr-remove-comment.sh comment-1234567890-1234567890
#
# This script removes a previously queued comment before it's submitted.
# Useful if the agent realizes it made a mistake or wants to update a comment.
#
# Environment variables (set by the composite action):
# PR_REVIEW_COMMENTS_DIR - Directory containing comment files (default: /tmp/pr-review-comments)
set -e
COMMENTS_DIR="${PR_REVIEW_COMMENTS_DIR:-/tmp/pr-review-comments}"
if [ ! -d "${COMMENTS_DIR}" ]; then
echo "No comments directory found: ${COMMENTS_DIR}"
exit 0
fi
# Check if first argument looks like a comment ID
if [[ "$1" =~ ^comment- ]]; then
COMMENT_ID="$1"
COMMENT_FILE="${COMMENTS_DIR}/${COMMENT_ID}.json"
if [ -f "${COMMENT_FILE}" ]; then
FILE=$(jq -r '._meta.file // .path' "${COMMENT_FILE}")
LINE=$(jq -r '._meta.line // .line' "${COMMENT_FILE}")
rm -f "${COMMENT_FILE}"
echo "✓ Removed comment ${COMMENT_ID} for ${FILE}:${LINE}"
else
echo "Comment not found: ${COMMENT_ID}"
exit 1
fi
else
# Treat as file and line number
FILE="$1"
LINE="$2"
if [ -z "$FILE" ] || [ -z "$LINE" ]; then
echo "Usage:"
echo " pr-remove-comment.sh <file> <line-number>"
echo " pr-remove-comment.sh <comment-id>"
echo ""
echo "Examples:"
echo " pr-remove-comment.sh src/main.go 42"
echo " pr-remove-comment.sh comment-1234567890-1234567890"
exit 1
fi
# Validate line is a positive integer (>= 1)
if ! [[ "$LINE" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: Line number must be a positive integer (>= 1), got: $LINE"
exit 1
fi
# Find and remove matching comment files
# Use nullglob to handle case where no files match
shopt -s nullglob
REMOVED=0
for COMMENT_FILE in "${COMMENTS_DIR}"/comment-*.json; do
COMMENT_FILE_PATH=$(jq -r '._meta.file // .path' "${COMMENT_FILE}")
COMMENT_LINE=$(jq -r '._meta.line // .line' "${COMMENT_FILE}")
if [ "$COMMENT_FILE_PATH" = "$FILE" ] && [ "$COMMENT_LINE" = "$LINE" ]; then
COMMENT_ID=$(basename "${COMMENT_FILE}" .json)
rm -f "${COMMENT_FILE}"
echo "✓ Removed comment ${COMMENT_ID} for ${FILE}:${LINE}"
REMOVED=$((REMOVED + 1))
fi
done
if [ "$REMOVED" -eq 0 ]; then
echo "No comment found for ${FILE}:${LINE}"
exit 1
fi
fi

143
.github/scripts/pr-review/pr-review.sh vendored Executable file
View file

@ -0,0 +1,143 @@
#!/bin/bash
# pr-review.sh - Submit a PR review (approve, request changes, or comment)
#
# Usage: pr-review.sh <APPROVE|REQUEST_CHANGES|COMMENT> [review-body]
# Example: pr-review.sh REQUEST_CHANGES "Please fix the issues noted above"
#
# This script creates and submits a review with any queued inline comments.
# Comments are read from individual files in PR_REVIEW_COMMENTS_DIR (created by pr-comment.sh).
#
# The review body can contain special characters (backticks, dollar signs, etc.)
# and will be safely passed to the GitHub API without shell interpretation.
#
# Environment variables (set by the composite action):
# PR_REVIEW_REPO - Repository (owner/repo)
# PR_REVIEW_PR_NUMBER - Pull request number
# PR_REVIEW_HEAD_SHA - HEAD commit SHA
# PR_REVIEW_COMMENTS_DIR - Directory containing queued comment files (default: /tmp/pr-review-comments)
set -e
# Configuration from environment
REPO="${PR_REVIEW_REPO:?PR_REVIEW_REPO environment variable is required}"
PR_NUMBER="${PR_REVIEW_PR_NUMBER:?PR_REVIEW_PR_NUMBER environment variable is required}"
HEAD_SHA="${PR_REVIEW_HEAD_SHA:?PR_REVIEW_HEAD_SHA environment variable is required}"
COMMENTS_DIR="${PR_REVIEW_COMMENTS_DIR:-/tmp/pr-review-comments}"
# Arguments
EVENT="$1"
shift 2>/dev/null || true
# Read body from remaining arguments
# Join all remaining arguments with spaces, preserving the string as-is
BODY="$*"
if [ -z "$EVENT" ]; then
echo "Usage: pr-review.sh <APPROVE|REQUEST_CHANGES|COMMENT> [review-body]"
echo "Example: pr-review.sh REQUEST_CHANGES 'Please fix the issues noted in the inline comments'"
exit 1
fi
# Validate event type
case "$EVENT" in
APPROVE|REQUEST_CHANGES|COMMENT)
;;
*)
echo "Error: Invalid event type '${EVENT}'"
echo "Must be one of: APPROVE, REQUEST_CHANGES, COMMENT"
exit 1
;;
esac
# Read queued comments from individual files
COMMENTS="[]"
COMMENT_COUNT=0
if [ -d "${COMMENTS_DIR}" ]; then
# Collect all comment files and merge into a single JSON array
# Remove _meta fields before submitting (they're only for internal use)
COMMENT_FILES=("${COMMENTS_DIR}"/comment-*.json)
if [ -f "${COMMENT_FILES[0]}" ]; then
# Use jq to read all comment files, extract the comment data (without _meta), and combine
COMMENTS=$(jq -s '[.[] | del(._meta)]' "${COMMENTS_DIR}"/comment-*.json)
COMMENT_COUNT=$(echo "$COMMENTS" | jq 'length')
if [ "$COMMENT_COUNT" -gt 0 ]; then
echo "Found ${COMMENT_COUNT} queued inline comment(s)"
fi
fi
fi
# Append standard footer to the review body (if body is provided)
FOOTER='
---
Marvin Context Protocol | Type `/marvin` to interact further
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.'
if [ -n "$BODY" ]; then
BODY_WITH_FOOTER="${BODY}${FOOTER}"
else
BODY_WITH_FOOTER=""
fi
# Build the review request JSON
# Use jq to safely construct the JSON with all special characters handled
REVIEW_JSON=$(jq -n \
--arg commit_id "$HEAD_SHA" \
--arg event "$EVENT" \
--arg body "$BODY_WITH_FOOTER" \
--argjson comments "$COMMENTS" \
'{
commit_id: $commit_id,
event: $event,
comments: $comments
} + (if $body != "" then {body: $body} else {} end)')
# Check if HEAD has changed since review started (race condition detection)
CURRENT_HEAD=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha')
if [ "$CURRENT_HEAD" != "$HEAD_SHA" ]; then
echo "⚠️ WARNING: PR head has changed since review started!"
echo " Review started at: ${HEAD_SHA:0:7}"
echo " Current head: ${CURRENT_HEAD:0:7}"
echo ""
echo " New commits may have shifted line numbers. Review will be submitted"
echo " against the original commit (${HEAD_SHA:0:7}) but comments may be outdated."
echo ""
fi
echo "Submitting ${EVENT} review for commit ${HEAD_SHA:0:7}..."
# Create and submit the review in one API call
# Use a temp file to safely pass the JSON body
TEMP_JSON=$(mktemp)
trap "rm -f ${TEMP_JSON}" EXIT
echo "$REVIEW_JSON" > "${TEMP_JSON}"
RESPONSE=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/reviews" \
-X POST \
--input "${TEMP_JSON}" 2>&1) || {
echo "Error submitting review:"
echo "$RESPONSE"
exit 1
}
# Clean up the comments directory after successful submission
if [ -d "${COMMENTS_DIR}" ] && [ "$COMMENT_COUNT" -gt 0 ]; then
rm -f "${COMMENTS_DIR}"/comment-*.json
# Remove directory if empty
rmdir "${COMMENTS_DIR}" 2>/dev/null || true
fi
REVIEW_URL=$(echo "$RESPONSE" | jq -r '.html_url // empty')
REVIEW_STATE=$(echo "$RESPONSE" | jq -r '.state // empty')
if [ -n "$REVIEW_URL" ]; then
echo "✓ Review submitted (${REVIEW_STATE}): ${REVIEW_URL}"
if [ "$COMMENT_COUNT" -gt 0 ]; then
echo " Included ${COMMENT_COUNT} inline comment(s)"
fi
else
echo "✓ Review submitted successfully"
fi

View file

@ -1,178 +0,0 @@
name: Martian Issue Triage
on:
issues:
types: [opened, labeled]
jobs:
martian-issue-triage:
# For labeled events, verify the labeler is a repo member to prevent privilege escalation
if: |
(github.event.action == 'opened' && contains(fromJSON('["strawgate", "jlowin"]'), github.actor)) ||
(github.event.action == 'labeled' && github.event.label.name == 'triage-martian' && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.sender.author_association))
concurrency:
group: triage-martian-${{ github.event.issue.number }}
cancel-in-progress: true
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
pull-requests: read
id-token: write
steps:
- name: Checkout base repository
uses: actions/checkout@v6
with:
repository: ${{ github.repository }}
ref: ${{ github.event.repository.default_branch }}
# Install UV package manager
- name: Install UV
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
- name: Set triage prompt
id: triage-prompt
run: |
cat >> $GITHUB_OUTPUT << 'EOF'
PROMPT<<PROMPT_END
You're an issue triage assistant for FastMCP, a Python framework for building Model Context Protocol servers and clients.
# IMPORTANT RULES
1. You will not make branches or pull requests. Your ONLY action will be investigating the issue, locating related issues,
pull requests, and files in the repository and reporting your findings.
2. You will identify the issue type (bug/feature/question) up front and tailor the Recommendation (e.g., for questions: answer directly + links; for bugs: point to failing tests/lines).
3. You will avoid speculation and only assert facts that are deeply rooted (traceable) to the codebase, language/framework conventions, related issues, related pull requests, etc.
4. The main branch of the repository has been cloned locally, but changes will not be accepted and you are not allowed to make pull requests or other changes. You can search the local repository for relevant code. You will use the available MCP Server tools identify related issues and pull requests (search_issues and search_pull_requests) and you can use search_code to look at the code in relevant dependent packages. For example, you can use search_code to look at the underlying SDK `https://github.com/modelcontextprotocol/python-sdk` to see how it implements a certain class or function relevant to the issue at hand.
5. You cannot modify GitHub Workflows directly, you will have to create the updated workflow in a `github` folder and tell the maintainer to relocate it for you.
# Getting Started
1. Call the generate_agents_md tool to get a high-level summary of the project you're working in
2. Get the issue ${{ github.event.issue.number }} in the GitHub repository: ${{ github.repository }}.
3. Use the search_issues and search_pull_requests tools to scour the repository for actually related issues and pull requests
4. Call the search_code, get_files, etc. tools to search the repository to identify the related classes, methods, docs, tests, etc that are relevant to the issue.
# Providing a Great Response
Your number one priority is to provide a great response to the issue. A great response is a response that is clear, concise, accurate, and actionable. You will avoid long paragraphs, flowery language, and overly verbose responses. Your readers have limited time and attention, so you will be concise and to the point.
In priority order your goal is to:
1. Provide context about the request or issue (related issues, pull requests, files, etc.)
2. Layout a single high-quality and actionable recommendation for how to address the issue based on your knowledge of the project, codebase, and issue
3. Provide an high quality and detailed plan that a junior developer could follow to implement the recommendation
Populate the following sections in your response:
Recommendation (or “No recommendation” with reason)
Findings
Detailed Action Plan
Related Items
Related Files
Related Webpages
You may not be able to do all of these things, sometimes you may find that all you can do is provide in-depth context of the issue and related items. That's perfectly acceptable and expected. Your performance is judged by how accurate your findings are, do the investigation required to have high confidence in your findings and recommendations. "I don't know" or "I'm unable to recommend a course of action" is better than a bad or wrong answer.
When formulating your response, you will never "bury the lede", you will always provide a clear and concise tl;dr as the first thing in your response. As your response grows in length you can organize the more detailed parts of your response collapsible sections using <details> and <summary> tags. You shouldn't put everything in collapsible sections, especially if the response is short. Use your discretion to determine when to use collapsible sections to avoid overwhelming the reader with too much detail -- think of them like an appendix that can be expanded if the reader is interested.
# Example output for "Recommendation" part of the response
PR #654 already implements the requested feature but is incomplete. The Pull Request is not in a mergeable state yet, the remaining work should be completed: 1) update the Calculator.divide method to utilize the new DivisionByZeroError or the safe_divide function, and 2) update the tests to ensure that the Calculator.divide method raises the new DivisionByZeroError when the divisor is 0.
<details>
<summary>Findings</summary>
...details from the code analysis that are relevant to the issue and the recommendation...
</details>
<details>
<summary>Detailed Action Plan</summary>
...a detailed plan that a junior developer could follow to implement the recommendation...
</details>
# Example Output for "Related Items" part of the response
<details>
<summary>Related Issues and Pull Requests</summary>
| Repository | Issue or PR | Relevance |
| --- | --- | --- |
| jlowin/fastmcp | [Add matrix operations support](https://github.com/jlowin/fastmcp/pull/680) | This pull request directly addresses the feature request for adding matrix operations to the calculator. |
| jlowin/fastmcp | [Add matrix operations support](https://github.com/jlowin/fastmcp/issues/681) | This issue directly addresses the feature request for adding matrix operations to the calculator. |
</details>
<details>
<summary>Related Files</summary>
| Repository | File | Relevance | Sections |
| --- | --- | --- | --- |
| modelcontextprotocol/python-sdk | [test_calculator.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/test_calculator.py) | This file contains the test cases for the Calculator class, including a test that specifically asserts a ValueError is raised for division by zero, confirming the current intended behavior. | [25-27](https://github.com/modelcontextprotocol/python-sdk/blob/main/test_calculator.py#L25-L27) |
| modelcontextprotocol/python-sdk | [calculator.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/calculator.py) | This file contains the implementation of the Calculator class, specifically the `divide` method which raises the ValueError when dividing by zero, matching the bug report. | [29-32](https://github.com/modelcontextprotocol/python-sdk/blob/main/calculator.py#L29-L32) |
</details>
<details>
<summary>Related Webpages</summary>
| Name | URL | Relevance |
| --- | --- | --- |
| Handling Division by Zero Best Practices | https://my-blog-about-division-by-zero.com/handling+division+by+zero+in+calculator | This webpage provides general best practices for handling division by zero in calculator applications and in Python, which is directly relevant to the issue and potential solutions. |
</details>
PROMPT_END
EOF
- name: Setup GitHub MCP Server
run: |
mkdir -p /tmp/mcp-config
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
{
"mcpServers": {
"repository-summary": {
"type": "http",
"url": "https://agents-md-generator.fastmcp.app/mcp"
},
"code-search": {
"type": "http",
"url": "https://public-code-search.fastmcp.app/mcp"
},
"github-research": {
"type": "stdio",
"command": "uvx",
"args": [
"github-research-mcp"
],
"env": {
"DISABLE_SUMMARIES": "true",
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ steps.marvin-token.outputs.token }}"
}
}
}
}
EOF
- name: Clean up stale Claude locks
run: rm -rf ~/.claude/.locks ~/.local/state/claude/locks || true
- name: Run Martian for Issue Triage
uses: anthropics/claude-code-action@v1
with:
github_token: ${{ steps.marvin-token.outputs.token }}
bot_name: "Marvin Context Protocol"
prompt: ${{ steps.triage-prompt.outputs.PROMPT }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }}
track_progress: true
claude_args: |
--model claude-sonnet-4-5-20250929
--allowedTools mcp__repository-summary,mcp__code-search__search_code,mcp__github-research__get_repository,mcp__github-research__get_issue,mcp__github-research__get_pull_request,mcp__github-research__search_issues,mcp__github-research__search_pull_requests,mcp__github-research__get_files
--mcp-config /tmp/mcp-config/mcp-servers.json
settings: |
{
"GH_TOKEN": "${{ steps.marvin-token.outputs.token }}"
}

View file

@ -0,0 +1,204 @@
# Triage new issues: investigate, recommend, apply labels
# Calls run-claude directly with triage prompt (elastic issue-triage style)
name: Triage Issue
on:
issues:
types: [opened]
jobs:
triage:
if: |
contains(fromJSON('["jlowin", "strawgate"]'), github.event.issue.user.login)
concurrency:
group: triage-issue-${{ github.event.issue.number }}
cancel-in-progress: true
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
pull-requests: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
repository: ${{ github.repository }}
ref: ${{ github.event.repository.default_branch }}
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
- name: React to issue with eyes
env:
GH_TOKEN: ${{ steps.marvin-token.outputs.token }}
run: |
gh api "repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/reactions" -f content=eyes 2>/dev/null || true
- name: Run Claude for Triage
uses: ./.github/actions/run-claude
with:
claude-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github-token: ${{ steps.marvin-token.outputs.token }}
allowed-tools: "Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code"
prompt: |
<context>
Repository: ${{ github.repository }}
Issue Number: #${{ github.event.issue.number }}
Issue Title: ${{ github.event.issue.title }}
Issue Author: ${{ github.event.issue.user.login }}
</context>
<issue_body>
${{ github.event.issue.body }}
</issue_body>
<task>
Triage this new GitHub issue and provide a helpful, actionable response. You can write files and execute commands to test, verify, or investigate the issue.
</task>
<constraints>
This workflow is for investigation, testing, and planning.
You CANNOT: Create branches, checkout branches, commit code to the repository
Do not push changes to the repository.
You CAN: Read/analyze code, search repository, review git history, search for similar issues, write files, verify behavior, provide analysis and recommendations
</constraints>
<allowed_tools>
You have access to the following tools (comma-separated list):
Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code
You can only use tools that are explicitly listed above. For Bash commands, the pattern `Bash(command:*)` means you can run that command with any arguments. If a command is not listed, it is not available.
</allowed_tools>
<getting_started>
Use `mcp__agents-md-generator__generate_agents_md` to get repository context before triaging.
</getting_started>
<investigation_tools>
- `mcp__public-code-search__search_code`: Search code in OTHER repositories (use `Grep`/`Read` for this repo)
- `WebSearch`: Search the web for documentation, best practices, or solutions
- `WebFetch`: Fetch and read content from URLs
- Git commands: You have access to git commands, but write commands (commit, push, checkout, branch creation) are blocked
- Write: You can write files (e.g., test files, temporary files for verification)
- Execution: See `<allowed_tools>` section above for exact list of available execution commands
</investigation_tools>
<execution_guidelines>
If execution commands are available (check `<allowed_tools>` section), you can:
- Run tests to verify reported bugs or test proposed solutions
- Execute scripts to understand behavior
- Run linters or static analysis tools
- Verify environment setup or dependencies
- Test specific code paths or scenarios
- Write test files to confirm behavior
When executing commands:
- Explain what you're testing and why
- Include command output in your response when relevant
- Use execution to validate your findings and recommendations
- Only use commands that are explicitly listed in `<allowed_tools>`
</execution_guidelines>
<response_goals>
Your number one priority is to provide a great response to the issue. A great response is a response that is clear, concise, accurate, and actionable. You will avoid long paragraphs, flowery language, and overly verbose responses. Your readers have limited time and attention, so you will be concise and to the point.
In priority order your goal is to:
1. Provide context about the request or issue (related issues, pull requests, files, etc.)
2. Layout a single high-quality and actionable recommendation for how to address the issue based on your knowledge of the project, codebase, and issue
3. Provide a high quality and detailed plan that a junior developer could follow to implement the recommendation
4. Use execution to verify findings when appropriate (check `<allowed_tools>` section for available commands)
</response_goals>
<response_sections>
Populate the following sections in your response:
Recommendation (or "No recommendation" with reason)
Findings
Verification (if you executed tests or commands - check `<allowed_tools>` section)
Detailed Action Plan
Related Items
Related Files
Related Webpages
You may not be able to do all of these things, sometimes you may find that all you can do is provide in-depth context of the issue and related items. That's perfectly acceptable and expected. Your performance is judged by how accurate your findings are, do the investigation required to have high confidence in your findings and recommendations. "I don't know" or "I'm unable to recommend a course of action" is better than a bad or wrong answer.
When formulating your response, you will never "bury the lede", you will always provide a clear and concise tl;dr as the first thing in your response. As your response grows in length you can organize the more detailed parts of your response collapsible sections using <details> and <summary> tags. You shouldn't put everything in collapsible sections, especially if the response is short. Use your discretion to determine when to use collapsible sections to avoid overwhelming the reader with too much detail -- think of them like an appendix that can be expanded if the reader is interested.
</response_sections>
<response_examples>
# Example output for "Recommendation" part of the response
PR #654 already implements the requested feature but is incomplete. The Pull Request is not in a mergeable state yet, the remaining work should be completed: 1) update the Calculator.divide method to utilize the new DivisionByZeroError or the safe_divide function, and 2) update the tests to ensure that the Calculator.divide method raises the new DivisionByZeroError when the divisor is 0.
<details>
<summary>Findings</summary>
...details from the code analysis that are relevant to the issue and the recommendation...
</details>
<details>
<summary>Verification</summary>
I ran the existing tests (if execution commands are available in `<allowed_tools>`) and confirmed the current behavior:
```bash
$ pytest test_calculator.py::test_divide_by_zero
FAILED - raises ValueError instead of DivisionByZeroError
```
This confirms the issue report is accurate.
</details>
<details>
<summary>Detailed Action Plan</summary>
...a detailed plan that a junior developer could follow to implement the recommendation...
</details>
# Example Output for "Related Items" part of the response
<details>
<summary>Related Issues and Pull Requests</summary>
| Repository | Issue or PR | Relevance |
| --- | --- | --- |
| jlowin/fastmcp | [Add matrix operations support](https://github.com/jlowin/fastmcp/pull/680) | This pull request directly addresses the feature request for adding matrix operations to the calculator. |
| jlowin/fastmcp | [Add matrix operations support](https://github.com/jlowin/fastmcp/issues/681) | This issue directly addresses the feature request for adding matrix operations to the calculator. |
</details>
<details>
<summary>Related Files</summary>
| Repository | File | Relevance | Sections |
| --- | --- | --- | --- |
| modelcontextprotocol/python-sdk | [test_calculator.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/test_calculator.py) | This file contains the test cases for the Calculator class, including a test that specifically asserts a ValueError is raised for division by zero, confirming the current intended behavior. | [25-27](https://github.com/modelcontextprotocol/python-sdk/blob/main/test_calculator.py#L25-L27) |
| modelcontextprotocol/python-sdk | [calculator.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/calculator.py) | This file contains the implementation of the Calculator class, specifically the `divide` method which raises the ValueError when dividing by zero, matching the bug report. | [29-32](https://github.com/modelcontextprotocol/python-sdk/blob/main/calculator.py#L29-L32) |
</details>
<details>
<summary>Related Webpages</summary>
| Name | URL | Relevance |
| --- | --- | --- |
| Handling Division by Zero Best Practices | https://my-blog-about-division-by-zero.com/handling+division+by+zero+in+calculator | This webpage provides general best practices for handling division by zero in calculator applications and in Python, which is directly relevant to the issue and potential solutions. |
</details>
</response_examples>
<response_footer>
Always end your comment with a new line, three dashes, and the footer message:
<exact_content>
---
Marvin Context Protocol | Type `/marvin` to interact further
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.
</exact_content>
</response_footer>
<github_formatting>
When writing GitHub comments, wrap branch names, tags, or other @-references in backticks (e.g., `@main`, `@v1.0`) to avoid accidentally pinging users. Do not add backticks around terms that are already inside backticks or code blocks.
</github_formatting>

View file

@ -0,0 +1,144 @@
# Respond to /marvin mentions in issue comments (elastic mention-in-issue style)
# Calls run-claude directly
name: Comment on Issue
on:
issue_comment:
types: [created]
permissions:
contents: write
issues: write
pull-requests: read
id-token: write
jobs:
comment:
if: |
!github.event.issue.pull_request &&
contains(github.event.comment.body, '/marvin') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install UV
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install dependencies
run: uv sync --python 3.12
- name: Run prek
uses: j178/prek-action@v1
env:
SKIP: no-commit-to-branch
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
- name: React to comment with eyes
env:
GH_TOKEN: ${{ steps.marvin-token.outputs.token }}
run: |
gh api "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" -f content=eyes 2>/dev/null || true
- name: Run Claude for Issue Comment
uses: ./.github/actions/run-claude
with:
claude-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github-token: ${{ steps.marvin-token.outputs.token }}
trigger-phrase: "/marvin"
allowed-bots: "*"
allowed-tools: "Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code"
prompt: |
<context>
Repository: ${{ github.repository }}
Issue Number: #${{ github.event.issue.number }}
Issue Title: ${{ github.event.issue.title }}
Issue Author: ${{ github.event.issue.user.login }}
Comment Author: ${{ github.event.comment.user.login }}
</context>
<user_request>
${{ github.event.comment.body }}
</user_request>
<task>
You have been mentioned in a GitHub issue comment. Understand the request, gather context, complete the task, and respond with results.
</task>
<constraints>
This workflow allows read, write, and execute capabilities but cannot push changes.
You CAN: Read/analyze code, modify files, write code, run tests, execute commands
You CANNOT: Commit code, push changes, create branches, checkout branches, create pull requests
**Important**: You cannot push changes to the repository - you can only make changes locally and provide feedback or recommendations.
</constraints>
<allowed_tools>
You have access to the following tools (comma-separated list):
Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code
You can only use tools that are explicitly listed above. For Bash commands, the pattern `Bash(command:*)` means you can run that command with any arguments. If a command is not listed, it is not available.
</allowed_tools>
<getting_started>
Use `mcp__agents-md-generator__generate_agents_md` to get repository context before responding.
</getting_started>
<investigation_approach>
Be thorough in your investigations:
- Understand the full context of the repository
- Review related code, issues, and PRs
- Consider edge cases and implications
- Gather all relevant information before responding
Available tools:
- `mcp__public-code-search__search_code`: Search code in OTHER repositories (use `Grep`/`Read` for this repo)
- `WebSearch`: Search the web for documentation, best practices, or solutions
- `WebFetch`: Fetch and read content from URLs
</investigation_approach>
<common_tasks>
- Answer questions about the codebase
- Help debug reported problems (make changes locally to test, cannot push)
- Suggest solutions or workarounds
- Provide code examples
- Help clarify requirements
- Link to relevant documentation or code
</common_tasks>
<response_guidelines>
- Be concise and actionable
- If the request is unclear, ask clarifying questions
- If the request requires actions you cannot perform (like pushing changes), explain what you can and cannot do
- When making code changes, explain that they are local only and cannot be pushed
</response_guidelines>
<response_footer>
Always end your comment with a new line, three dashes, and the footer message:
<exact_content>
---
Marvin Context Protocol | Type `/marvin` to interact further
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.
</exact_content>
</response_footer>
<github_formatting>
When writing GitHub comments, wrap branch names, tags, or other @-references in backticks (e.g., `@main`, `@v1.0`) to avoid accidentally pinging users. Do not add backticks around terms that are already inside backticks or code blocks.
</github_formatting>

View file

@ -0,0 +1,284 @@
# Respond to /marvin mentions in PR review comments and issue comments on PRs
# Calls run-claude directly
name: Comment on PR
on:
issue_comment:
types: [created]
permissions:
contents: write
pull-requests: write
issues: read
id-token: write
jobs:
comment:
if: |
github.event.issue.pull_request &&
contains(github.event.comment.body, '/marvin') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout PR head branch
uses: actions/checkout@v6
with:
# do not set to pull_request.head.ref, claude will pull the branch if needed
fetch-depth: 0
- name: Install UV
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install dependencies
run: uv sync --python 3.12
- name: Run prek
uses: j178/prek-action@v1
env:
SKIP: no-commit-to-branch
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
- name: React to comment with eyes
env:
GH_TOKEN: ${{ steps.marvin-token.outputs.token }}
run: |
gh api "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" -f content=eyes 2>/dev/null || true
- name: Get PR HEAD SHA
id: pr-info
env:
GH_TOKEN: ${{ steps.marvin-token.outputs.token }}
run: |
PR_NUMBER="${{ github.event.issue.number }}"
HEAD_SHA=$(gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}" --jq '.head.sha')
echo "head_sha=${HEAD_SHA}" >> "$GITHUB_OUTPUT"
echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
- name: Run Claude for PR Comment
uses: ./.github/actions/run-claude
env:
MENTION_REPO: ${{ github.repository }}
MENTION_PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
MENTION_SCRIPTS: ${{ github.workspace }}/.github/scripts/mention
PR_REVIEW_REPO: ${{ github.repository }}
PR_REVIEW_PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
PR_REVIEW_HEAD_SHA: ${{ steps.pr-info.outputs.head_sha }}
PR_REVIEW_COMMENTS_DIR: /tmp/pr-review-comments
PR_REVIEW_HELPERS_DIR: ${{ github.workspace }}/.github/scripts/pr-review
with:
claude-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github-token: ${{ steps.marvin-token.outputs.token }}
trigger-phrase: "/marvin"
allowed-bots: "*"
allowed-tools: "Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code"
prompt: |
<context>
Repository: ${{ github.repository }}
PR Number: #${{ steps.pr-info.outputs.pr_number }}
PR Title: ${{ github.event.issue.title }}
PR Author: ${{ github.event.issue.user.login }}
Comment Author: ${{ github.event.comment.user.login }}
**Note**: The PR head branch has already been checked out. The workspace is ready - you can immediately start working on the PR code.
</context>
<user_request>
${{ github.event.comment.body }}
</user_request>
<task>
You have been mentioned in a Pull Request comment. Understand the request, gather context, complete the task, and respond with results.
</task>
<constraints>
This workflow allows read, write, and execute capabilities but cannot push changes.
You CAN: Read/analyze code, modify files, write code, run tests, execute commands, resolve review threads
You CANNOT: Commit code, push changes, create branches, checkout branches, create pull requests
**Important**: You cannot push changes to the repository - you can only make changes locally and provide feedback or recommendations.
</constraints>
<allowed_tools>
You have access to the following tools (comma-separated list):
Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code
You can only use tools that are explicitly listed above. For Bash commands, the pattern `Bash(command:*)` means you can run that command with any arguments. If a command is not listed, it is not available.
</allowed_tools>
<getting_started>
Use `mcp__agents-md-generator__generate_agents_md` to get repository context before responding.
</getting_started>
<investigation_approach>
Be thorough in your investigations:
- Understand the full context of the repository
- Review related code, issues, and PRs
- Consider edge cases and implications
- Gather all relevant information before responding
Available tools:
- `mcp__public-code-search__search_code`: Search code in OTHER repositories (use `Grep`/`Read` for this repo)
- `WebSearch`: Search the web for documentation, best practices, or solutions
- `WebFetch`: Fetch and read content from URLs
</investigation_approach>
<common_tasks>
- Address review feedback and fix issues (make changes locally, cannot push)
- Answer questions about the changes
- Make additional code changes (local only)
- Resolve review threads after addressing feedback (if changes are made separately)
- Perform PR reviews when asked (use the PR review process below)
</common_tasks>
<pr_review_guidance>
When asked to review this PR, follow this structured review process.
The `$PR_REVIEW_HELPERS_DIR` environment variable is pre-configured for all scripts below.
<review_process>
Follow these steps in order:
**Step 1: Gather context**
- Use `mcp__agents-md-generator__generate_agents_md` to get repository context
(if this fails, explore the repository to understand the codebase — read key files like README, CONTRIBUTING, etc.)
- Run `$PR_REVIEW_HELPERS_DIR/pr-existing-comments.sh --summary` to see existing review threads per file
- Run `$PR_REVIEW_HELPERS_DIR/pr-diff.sh` to see changed files with line-numbered diffs
(for large PRs, this lists files only — review each with `pr-diff.sh <filename>`)
**Step 2: Review each file**
For each changed file:
a. If the summary showed existing threads for this file, first run:
`$PR_REVIEW_HELPERS_DIR/pr-existing-comments.sh --file <path>`
Read the full thread details. The output uses these conventions:
- `← has replies` — a conversation happened; read carefully before commenting
- `[truncated]` — comment was cut short; add `--full` if you need the complete text to understand the comment
- `[abc1234]` — commit the comment was made on; use `git show abc1234` if needed
- `~42` — approximate line from an older revision (exact line no longer maps to current diff)
b. Review the diff. Use `Read` to see full file contents when you need more context.
Identify issues matching review_criteria. Do NOT flag:
- Issues in unchanged code (only review the diff)
- Style preferences handled by linters
- Pre-existing issues not introduced by this PR
- Issues already covered by existing threads (see below)
**Existing thread rules** (check BEFORE leaving any comment):
- Resolved with reviewer reply → reviewer's decision is final. Do NOT re-flag.
Examples: "It should remain as X", "This is intentional", "No need to do this change"
- Resolved without reply → author likely fixed it. Do NOT re-raise unless the fix introduced a new problem.
- Unresolved → already flagged. Do NOT re-comment. Mention in review body if you have more to add.
- Outdated → code changed. Only re-flag if the issue still applies to the current diff.
When in doubt, do not duplicate. Redundant comments erode trust in the review process.
**Step 3: Leave comments for NEW issues only**
For each genuinely new issue not covered by existing threads:
```bash
$PR_REVIEW_HELPERS_DIR/pr-comment.sh <file> <line> \
--severity <critical|high|medium|low|nitpick> \
--title "Brief description" \
--why "Risk or impact" <<'EOF'
corrected code here
EOF
```
Always provide suggestion code. Use `--no-suggestion` only when the fix requires
changes across multiple locations. Broader architectural concerns belong in the
review body, not inline comments.
To remove a queued comment: `$PR_REVIEW_HELPERS_DIR/pr-remove-comment.sh <file> <line>`
**Step 4: Submit the review**
```bash
$PR_REVIEW_HELPERS_DIR/pr-review.sh <APPROVE|REQUEST_CHANGES|COMMENT> "<review body>"
```
- REQUEST_CHANGES: Any 🔴 CRITICAL or 🟠 HIGH issues found
- COMMENT: 🟡 MEDIUM issues found (but no critical/high)
- APPROVE: No issues, or only ⚪ LOW / 💬 NITPICK suggestions
The review body should include broader architectural concerns not suited for inline comments.
Avoid summarizing the PR or offering praise. If approving with no issues, omit the review body.
A standard footer is automatically appended to all comments and reviews.
</review_process>
<severity_classification>
🔴 CRITICAL - Must fix before merge (security vulnerabilities, data corruption, production-breaking bugs)
🟠 HIGH - Should fix before merge (logic errors, missing validation, significant performance issues)
🟡 MEDIUM - Address soon, non-blocking (error handling gaps, suboptimal patterns, missing edge cases)
⚪ LOW - Author discretion, non-blocking (minor improvements, documentation, style not covered by linters)
💬 NITPICK - Truly optional (stylistic preferences, alternative approaches — safe to ignore)
</severity_classification>
<review_criteria>
Focus on these categories, in priority order:
1. Security vulnerabilities (injection, XSS, auth bypass, secrets exposure)
2. Logic bugs that could cause runtime failures or incorrect behavior
3. Data integrity issues (race conditions, missing transactions, corruption risk)
4. Performance bottlenecks (N+1 queries, memory leaks, blocking operations)
5. Error handling gaps (unhandled exceptions, missing validation)
6. Breaking changes to public APIs without migration path
7. Missing or incorrect test coverage for critical paths
</review_criteria>
</pr_review_guidance>
<review_thread_tools>
View unresolved review threads:
```bash
$MENTION_SCRIPTS/gh-get-review-threads.sh
```
Filter for unresolved threads from a specific reviewer:
```bash
$MENTION_SCRIPTS/gh-get-review-threads.sh "reviewer-username"
```
Resolve a review thread after addressing feedback:
```bash
$MENTION_SCRIPTS/gh-resolve-review-thread.sh "THREAD_ID" "Fixed by updating the error handling"
```
- `THREAD_ID` is the GraphQL node ID from the review threads output (e.g., `PRRT_kwDOABC123`)
- The comment is optional - use it to explain what you did
Note: Since you cannot push changes, you can resolve threads to acknowledge feedback, but actual fixes would need to be applied separately.
</review_thread_tools>
<response_guidelines>
- Be concise and actionable
- If the request is unclear, ask clarifying questions
- If the request requires actions you cannot perform (like pushing changes), explain what you can and cannot do
- When making code changes, explain that they are local only and cannot be pushed
**When performing a PR review**: Your substantive feedback belongs in the PR review submission
(via pr-review.sh), not in the comment response. The comment should only report:
- That you've submitted the review (with the outcome: approved, requested changes, etc.)
- Any issues encountered during the review process
- Brief status updates
Do NOT duplicate the review content in your comment - the review itself contains all the details.
Keep the comment short, e.g., "I've submitted my review requesting changes. See the review for details."
</response_guidelines>
<response_footer>
Always end your comment with a new line, three dashes, and the footer message:
<exact_content>
---
Marvin Context Protocol | Type `/marvin` to interact further
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.
</exact_content>
</response_footer>
<github_formatting>
When writing GitHub comments, wrap branch names, tags, or other @-references in backticks (e.g., `@main`, `@v1.0`) to avoid accidentally pinging users. Do not add backticks around terms that are already inside backticks or code blocks.
</github_formatting>

View file

@ -1,87 +0,0 @@
name: Marvin Context Protocol
on:
issue_comment: { types: [created] }
pull_request_review_comment: { types: [created] }
pull_request_review: { types: [submitted] }
pull_request: { types: [opened, edited] }
issues: { types: [opened, edited, assigned, labeled] }
discussion: { types: [created, edited, labeled] }
discussion_comment: { types: [created] }
permissions:
contents: write
issues: write
pull-requests: write
discussions: write
actions: read
id-token: write
jobs:
marvin:
# Restrict all triggers to repo members (OWNER, MEMBER, COLLABORATOR)
if: |
(
(github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion_comment') &&
contains(github.event.comment.body, '/marvin') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '/marvin') && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) ||
(github.event_name == 'pull_request' && contains(github.event.pull_request.body, '/marvin') && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)) ||
(github.event_name == 'issues' && contains(github.event.issue.body, '/marvin') && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association)) ||
(github.event_name == 'discussion' && contains(github.event.discussion.body, '/marvin') && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.discussion.author_association)) ||
(
github.event_name == 'issues' &&
((github.event.action == 'assigned' && github.event.assignee.login == 'Marvin Context Protocol') || (github.event.action == 'labeled' && github.event.label.name == 'marvin')) &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.sender.author_association)
)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
# Install UV package manager
- name: Install UV
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
# Install project dependencies
- name: Install dependencies
run: uv sync --python 3.12
- name: Run prek
uses: j178/prek-action@v1
env:
SKIP: no-commit-to-branch
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
- name: Clean up stale Claude locks
run: rm -rf ~/.claude/.locks ~/.local/state/claude/locks || true
# Marvin Assistant
- name: Run Marvin
uses: anthropics/claude-code-action@v1
with:
github_token: ${{ steps.marvin-token.outputs.token }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
trigger_phrase: "/marvin"
allowed_bots: "*"
claude_args: |
--allowedTools WebSearch,WebFetch,Bash(uv:*),Bash(pre-commit:*),Bash(prek:*),Bash(pytest:*),Bash(ruff:*),Bash(ty:*),Bash(git:*),Bash(gh:*),mcp__github__add_issue_comment,mcp__github__create_issue,mcp__github__get_issue,mcp__github__list_issues,mcp__github__search_issues,mcp__github__update_issue,mcp__github__update_issue_comment,mcp__github__create_pull_request,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_files,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__list_pull_requests,mcp__github__update_pull_request,mcp__github__update_pull_request_branch,mcp__github__update_pull_request_comment,mcp__github__merge_pull_request
additional_permissions: |
actions: read
settings: |
{
"model": "claude-sonnet-4-5-20250929",
"env": {
"GH_TOKEN": "${{ steps.marvin-token.outputs.token }}"
},
"customInstructions": "When you complete work on an issue: (1) You MUST create a pull request using the mcp__github__create_pull_request tool instead of posting a link, and (2) You MUST add the 'marvin-pr' label to the original issue using mcp__github__update_issue. Even if PR creation fails and you post a link instead, you MUST still add the 'marvin-pr' label. Follow the PR message guidelines in CLAUDE.md."
}

View file

@ -1,28 +1,28 @@
name: Update MCPServerConfig Schema
# This workflow runs on merges to main to automatically update the config schema
# by creating a PR when changes are needed.
# Regenerates config schema on PRs and commits it back to the branch,
# so the PR is self-contained and main is correct after merge.
on:
push:
pull_request:
branches: ["main"]
paths:
- "src/fastmcp/utilities/mcp_server_config/**"
- "!src/fastmcp/utilities/mcp_server_config/v1/schema.json" # Exclude the local schema file
- "!src/fastmcp/utilities/mcp_server_config/v1/schema.json"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
update-config-schema:
timeout-minutes: 5
runs-on: ubuntu-latest
if: >-
github.event_name == 'workflow_dispatch' ||
github.event.pull_request.head.repo.full_name == github.repository
steps:
- uses: actions/checkout@v6
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
@ -30,6 +30,11 @@ jobs:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
- uses: actions/checkout@v6
with:
ref: ${{ github.head_ref || github.ref }}
token: ${{ steps.marvin-token.outputs.token }}
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
@ -41,51 +46,22 @@ jobs:
- name: Generate config schema
run: |
echo "🔄 Generating fastmcp.json schema..."
# Generate schema in docs/public for web access
uv run python -c "
from fastmcp.utilities.mcp_server_config import generate_schema
generate_schema('docs/public/schemas/fastmcp.json/latest.json')
print('✅ Latest schema generated in docs/public')
"
# Also update the v1 schema in docs/public
uv run python -c "
from fastmcp.utilities.mcp_server_config import generate_schema
generate_schema('docs/public/schemas/fastmcp.json/v1.json')
print('✅ v1 schema generated in docs/public')
"
# Generate schema in the source directory for local development
uv run python -c "
from fastmcp.utilities.mcp_server_config import generate_schema
generate_schema('src/fastmcp/utilities/mcp_server_config/v1/schema.json')
print('✅ Schema generated in utilities/mcp_server_config/v1/')
"
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8
with:
token: ${{ steps.marvin-token.outputs.token }}
commit-message: "chore: Update fastmcp.json schema"
title: "chore: Update fastmcp.json schema"
body: |
This PR updates the fastmcp.json schema files to match the current source code.
The schema is automatically generated from `src/fastmcp/utilities/mcp_server_config/` to ensure consistency.
**Note:** This PR is fully automated and will update itself with any subsequent changes to the schema, or close automatically if the schema becomes up-to-date through other means. Feel free to leave it open until you're ready to merge.
🤖 Generated by Marvin
branch: marvin/update-config-schema
labels: |
ignore in release notes
delete-branch: true
author: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
committer: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
- name: Summary
- name: Commit and push if changed
run: |
echo "✅ Config schema generation workflow completed"
echo "PR will be created if there are changes, or closed if schema is already up to date"
git config user.name "marvin-context-protocol[bot]"
git config user.email "225465937+marvin-context-protocol[bot]@users.noreply.github.com"
git add docs/public/schemas/ src/fastmcp/utilities/mcp_server_config/v1/schema.json
if git diff --cached --quiet; then
echo "Config schema is up to date"
else
git commit -m "chore: Update fastmcp.json schema"
git push
echo "Config schema updated and pushed"
fi

View file

@ -1,10 +1,10 @@
name: Update SDK Documentation
# This workflow runs on merges to main to automatically update SDK docs
# by creating a PR when changes are needed.
# Regenerates SDK docs on PRs and commits them back to the branch,
# so the PR is self-contained and main is correct after merge.
on:
push:
pull_request:
branches: ["main"]
paths:
- "src/**"
@ -13,16 +13,16 @@ on:
permissions:
contents: write
pull-requests: write
jobs:
update-sdk-docs:
timeout-minutes: 5
runs-on: ubuntu-latest
if: >-
github.event_name == 'workflow_dispatch' ||
github.event.pull_request.head.repo.full_name == github.repository
steps:
- uses: actions/checkout@v6
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
@ -30,6 +30,11 @@ jobs:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
- uses: actions/checkout@v6
with:
ref: ${{ github.head_ref || github.ref }}
token: ${{ steps.marvin-token.outputs.token }}
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
@ -43,32 +48,17 @@ jobs:
uses: extractions/setup-just@v3
- name: Generate SDK documentation
run: just api-ref-all
- name: Commit and push if changed
run: |
echo "🔄 Generating SDK documentation..."
just api-ref-all
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8
with:
token: ${{ steps.marvin-token.outputs.token }}
commit-message: "chore: Update SDK documentation"
title: "chore: Update SDK documentation"
body: |
This PR updates the auto-generated SDK documentation to reflect the latest source code changes.
📚 Documentation is automatically generated from the source code docstrings and type annotations.
**Note:** This PR is fully automated and will update itself with any subsequent changes to the SDK, or close automatically if the documentation becomes up-to-date through other means. Feel free to leave it open until you're ready to merge.
🤖 Generated by Marvin
branch: marvin/update-sdk-docs
labels: |
ignore in release notes
delete-branch: true
author: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
committer: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
- name: Summary
run: |
echo "✅ SDK documentation generation workflow completed"
echo "PR will be created if there are changes, or closed if documentation is already up to date"
git config user.name "marvin-context-protocol[bot]"
git config user.email "225465937+marvin-context-protocol[bot]@users.noreply.github.com"
git add docs/python-sdk/
if git diff --cached --quiet; then
echo "SDK documentation is up to date"
else
git commit -m "chore: Update SDK documentation"
git push
echo "SDK documentation updated and pushed"
fi

View file

@ -96,7 +96,7 @@ When modifying MCP functionality, changes typically need to be applied across al
- Uses Mintlify framework
- Files must be in docs.json to be included
- Never modify `docs/python-sdk/**` (auto-generated)
- Do not manually modify `docs/python-sdk/**` — a bot automatically updates these files via commits added to PRs
- **Core Principle:** A feature doesn't exist unless it is documented!
### Documentation Guidelines

View file

@ -64,7 +64,7 @@ These compose cleanly, so complex patterns don't require complex code. And becau
## Installation
> [!Note]
> FastMCP 3.0 is currently in beta. Install with: `pip install fastmcp==3.0.0b2`
> FastMCP 3.0 is currently a release candidate. Install with: `pip install fastmcp==3.0.0rc1`
>
> For production systems requiring stability, pin to v2: `pip install 'fastmcp<3'`

View file

@ -4,6 +4,186 @@ icon: "list-check"
rss: true
---
<Update label="v3.0.0rc1" description="2026-02-12">
**[v3.0.0rc1: RC-ing is Believing](https://github.com/jlowin/fastmcp/releases/tag/v3.0.0rc1)**
FastMCP 3 RC1 means we believe the API is stable. Beta 2 drew a wave of real-world adoption — production deployments, migration reports, integration testing — and the feedback overwhelmingly confirmed that the architecture works. This release closes gaps that surfaced under load: auth flows that needed to be async, background tasks that needed reliable notification delivery, and APIs still carrying beta-era naming. If nothing unexpected surfaces, this is what 3.0.0 looks like.
🚨 **Breaking Changes** — The `ui=` parameter is now `app=` with a unified `AppConfig` class (matching the feature's actual name), and 16 `FastMCP()` constructor kwargs have finally been removed. If you've been ignoring months of deprecation warnings, you'll get a `TypeError` with specific migration instructions.
🔐 **Auth Improvements** — Three changes that together round out FastMCP's auth story for production. `auth=` checks can now be `async`, so you can hit databases or external services during authorization — previously, passing an async function silently passed because the unawaited coroutine was truthy. Static Client Registration lets clients provide a pre-registered `client_id`/`client_secret` directly, bypassing DCR for servers that don't support it. And Azure OBO flows are now declarative via dependency injection:
```python
from fastmcp.server.auth.providers.azure import EntraOBOToken
@mcp.tool()
async def get_emails(
graph_token: str = EntraOBOToken(["https://graph.microsoft.com/Mail.Read"]),
):
# OBO exchange already happened — just use the token
...
```
⚡ **Concurrent Sampling** — When an LLM returns multiple tool calls in a single response, `context.sample()` can now execute them in parallel. Opt in with `tool_concurrency=0` for unlimited parallelism, or set a bound. Tools that aren't safe to parallelize can declare `sequential=True`.
📡 **Background Task Notifications** — Background tasks now reliably push progress updates and elicit user input through the standard MCP protocol. A distributed Redis queue replaces polling (7,200 round-trips/hour → one blocking call), and `ctx.elicit()` in background tasks automatically relays through the client's standard `elicitation_handler`.
✅ **OpenAPI Output Validation** — When backends don't conform to their own OpenAPI schemas, the MCP SDK rejects the response and the tool fails. `validate_output=False` disables strict schema checking while still passing structured JSON to clients — a necessary escape hatch for imperfect APIs.
## What's Changed
### Enhancements 🔧
* generate-cli: auto-generate SKILL.md agent skill by [@jlowin](https://github.com/jlowin) in [#3115](https://github.com/jlowin/fastmcp/pull/3115)
* Scope Martian triage to bug-labeled issues for jlowin by [@jlowin](https://github.com/jlowin) in [#3124](https://github.com/jlowin/fastmcp/pull/3124)
* Add Azure OBO dependencies, auth token injection, and documentation by [@jlowin](https://github.com/jlowin) in [#2918](https://github.com/jlowin/fastmcp/pull/2918)
* feat: add Static Client Registration (#3085) by [@martimfasantos](https://github.com/martimfasantos) in [#3086](https://github.com/jlowin/fastmcp/pull/3086)
* Add concurrent tool execution with sequential flag by [@strawgate](https://github.com/strawgate) in [#3022](https://github.com/jlowin/fastmcp/pull/3022)
* Add validate_output option for OpenAPI tools by [@jlowin](https://github.com/jlowin) in [#3134](https://github.com/jlowin/fastmcp/pull/3134)
* Relay task elicitation through standard MCP protocol by [@chrisguidry](https://github.com/chrisguidry) in [#3136](https://github.com/jlowin/fastmcp/pull/3136)
* Bump py-key-value-aio to `>=0.4.0,<0.5.0` by [@strawgate](https://github.com/strawgate) in [#3143](https://github.com/jlowin/fastmcp/pull/3143)
* Support async auth checks by [@jlowin](https://github.com/jlowin) in [#3152](https://github.com/jlowin/fastmcp/pull/3152)
* Make $ref dereferencing optional via FastMCP(dereference_refs=...) by [@jlowin](https://github.com/jlowin) in [#3151](https://github.com/jlowin/fastmcp/pull/3151)
* Expose local_provider property, deprecate FastMCP.remove_tool() by [@jlowin](https://github.com/jlowin) in [#3155](https://github.com/jlowin/fastmcp/pull/3155)
* Add helpers for converting FunctionTool and TransformedTool to SamplingTool by [@strawgate](https://github.com/strawgate) in [#3062](https://github.com/jlowin/fastmcp/pull/3062)
* Updates to github actions / workflows for claude by [@strawgate](https://github.com/strawgate) in [#3157](https://github.com/jlowin/fastmcp/pull/3157)
### Fixes 🐞
* Updated deprecation URL for V3 by [@SrzStephen](https://github.com/SrzStephen) in [#3108](https://github.com/jlowin/fastmcp/pull/3108)
* Fix Windows test timeouts in OAuth proxy provider tests by [@strawgate](https://github.com/strawgate) in [#3123](https://github.com/jlowin/fastmcp/pull/3123)
* Fix session visibility marks leaking across sessions by [@jlowin](https://github.com/jlowin) in [#3132](https://github.com/jlowin/fastmcp/pull/3132)
* Fix unhandled exceptions in OpenAPI POST tool calls by [@jlowin](https://github.com/jlowin) in [#3133](https://github.com/jlowin/fastmcp/pull/3133)
* feat: distributed notification queue + BLPOP elicitation for background tasks by [@gfortaine](https://github.com/gfortaine) in [#2906](https://github.com/jlowin/fastmcp/pull/2906)
* fix: snapshot access token for background tasks (#3095) by [@gfortaine](https://github.com/gfortaine) in [#3138](https://github.com/jlowin/fastmcp/pull/3138)
* Stop duplicating path parameter descriptions into tool prose by [@jlowin](https://github.com/jlowin) in [#3149](https://github.com/jlowin/fastmcp/pull/3149)
* fix: guard client pagination loops against misbehaving servers by [@jlowin](https://github.com/jlowin) in [#3167](https://github.com/jlowin/fastmcp/pull/3167)
* Fix stale get_* references in docs and examples by [@jlowin](https://github.com/jlowin) in [#3168](https://github.com/jlowin/fastmcp/pull/3168)
* Support non-serializable values in Context.set_state by [@jlowin](https://github.com/jlowin) in [#3171](https://github.com/jlowin/fastmcp/pull/3171)
* Fix stale request context in StatefulProxyClient handlers by [@jlowin](https://github.com/jlowin) in [#3172](https://github.com/jlowin/fastmcp/pull/3172)
### Breaking Changes 🛫
* Rename ui= to app= and consolidate ToolUI/ResourceUI into AppConfig by [@jlowin](https://github.com/jlowin) in [#3117](https://github.com/jlowin/fastmcp/pull/3117)
* Remove deprecated FastMCP() constructor kwargs by [@jlowin](https://github.com/jlowin) in [#3148](https://github.com/jlowin/fastmcp/pull/3148)
### Docs 📚
* Update docs to reference beta 2 by [@jlowin](https://github.com/jlowin) in [#3112](https://github.com/jlowin/fastmcp/pull/3112)
* docs: add pre-registered OAuth clients to v3-features by [@jlowin](https://github.com/jlowin) in [#3129](https://github.com/jlowin/fastmcp/pull/3129)
### Dependencies 📦
* chore(deps): bump cryptography from 46.0.3 to 46.0.5 in /examples/testing_demo in the uv group across 1 directory by @dependabot in [#3140](https://github.com/jlowin/fastmcp/pull/3140)
### Other Changes 🦾
* docs: add v3.0.0rc1 features to v3-features tracking by [@jlowin](https://github.com/jlowin) in [#3145](https://github.com/jlowin/fastmcp/pull/3145)
* docs: remove nonexistent MSALApp from rc1 notes by [@jlowin](https://github.com/jlowin) in [#3146](https://github.com/jlowin/fastmcp/pull/3146)
## New Contributors
* [@martimfasantos](https://github.com/martimfasantos) made their first contribution in [#3086](https://github.com/jlowin/fastmcp/pull/3086)
**Full Changelog**: https://github.com/jlowin/fastmcp/compare/v3.0.0b2...v3.0.0rc1
</Update>
<Update label="v3.0.0b2" description="2026-02-07">
**[v3.0.0b2: 2 Fast 2 Beta](https://github.com/jlowin/fastmcp/releases/tag/v3.0.0b2)**
FastMCP 3 Beta 2 reflects the huge number of people that kicked the tires on Beta 1. Seven new contributors landed changes in this release, and early migration reports went smoother than expected, including teams on Prefect Horizon upgrading from v2. Most of Beta 2 is refinement: fixing what people found, filling gaps from real usage, hardening edges. But a few new features did land along the way.
🖥️ **Client CLI** — `fastmcp list`, `fastmcp call`, `fastmcp discover`, and `fastmcp generate-cli` turn any MCP server into something you can poke at from a terminal. Discover servers configured in Claude Desktop, Cursor, Goose, or project-level `mcp.json` files and reference them by name. `generate-cli` reads a server's schemas and writes a standalone typed CLI script where every tool is a proper subcommand with flags and help text.
🔐 **CIMD** (Client ID Metadata Documents) adds an alternative to Dynamic Client Registration for OAuth. Clients host a static JSON document at an HTTPS URL; that URL becomes the `client_id`. Server-side support includes SSRF-hardened fetching, cache-aware revalidation, and `private_key_jwt` validation. Enabled by default on `OAuthProxy`.
📱 **MCP Apps** — Spec-level compliance for the MCP Apps extension: `ui://` resource scheme, typed UI metadata on tools and resources, extension negotiation, and `ctx.client_supports_extension()` for runtime detection.
⏳ **Background Task Context** — `Context` now works transparently in Docket workers. `ctx.elicit()` routes through Redis-based coordination so background tasks can pause for user input without any code changes.
🛡️ **ResponseLimitingMiddleware** caps tool response sizes with UTF-8-safe truncation for text and schema-aware error handling for structured outputs.
🪿 **Goose Integration** — `fastmcp install goose` generates deeplink URLs for one-command server installation into Goose.
## What's Changed
### New Features 🎉
* Add MCP Apps Phase 1 — SDK compatibility (SEP-1865) by [@jlowin](https://github.com/jlowin) in [#3009](https://github.com/jlowin/fastmcp/pull/3009)
* Add `fastmcp list` and `fastmcp call` CLI commands by [@jlowin](https://github.com/jlowin) in [#3054](https://github.com/jlowin/fastmcp/pull/3054)
* Add `fastmcp generate-cli` command by [@jlowin](https://github.com/jlowin) in [#3065](https://github.com/jlowin/fastmcp/pull/3065)
* Add CIMD (Client ID Metadata Document) support for OAuth by [@jlowin](https://github.com/jlowin) in [#2871](https://github.com/jlowin/fastmcp/pull/2871)
### Enhancements 🔧
* Make duplicate bot less aggressive by [@jlowin](https://github.com/jlowin) in [#2981](https://github.com/jlowin/fastmcp/pull/2981)
* Remove uv lockfile monitoring from Dependabot by [@jlowin](https://github.com/jlowin) in [#2986](https://github.com/jlowin/fastmcp/pull/2986)
* Run static checks with --upgrade, remove lockfile check by [@jlowin](https://github.com/jlowin) in [#2988](https://github.com/jlowin/fastmcp/pull/2988)
* Adjust workflow triggers for Marvin by [@strawgate](https://github.com/strawgate) in [#3010](https://github.com/jlowin/fastmcp/pull/3010)
* Move tests to a reusable action and enable nightly checks by [@strawgate](https://github.com/strawgate) in [#3017](https://github.com/jlowin/fastmcp/pull/3017)
* feat: option to add upstream claims to the FastMCP proxy JWT by [@JonasKs](https://github.com/JonasKs) in [#2997](https://github.com/jlowin/fastmcp/pull/2997)
* Fix ty 0.0.14 compatibility and upgrade dependencies by [@jlowin](https://github.com/jlowin) in [#3027](https://github.com/jlowin/fastmcp/pull/3027)
* fix: automatically include offline_access as a scope in the Azure provider to enable automatic token refreshing by [@JonasKs](https://github.com/JonasKs) in [#3001](https://github.com/jlowin/fastmcp/pull/3001)
* feat: expand --reload to watch frontend file types by [@jlowin](https://github.com/jlowin) in [#3028](https://github.com/jlowin/fastmcp/pull/3028)
* Add `fastmcp install stdio` command by [@jlowin](https://github.com/jlowin) in [#3032](https://github.com/jlowin/fastmcp/pull/3032)
* Update martian-issue-triage.yml for Workflow editing guidance by [@strawgate](https://github.com/strawgate) in [#3033](https://github.com/jlowin/fastmcp/pull/3033)
* feat: Goose integration + dedicated install command by [@jlowin](https://github.com/jlowin) in [#3040](https://github.com/jlowin/fastmcp/pull/3040)
* Fixing spelling issues in multiple files by [@didier-durand](https://github.com/didier-durand) in [#2996](https://github.com/jlowin/fastmcp/pull/2996)
* Add `fastmcp discover` and name-based server resolution by [@jlowin](https://github.com/jlowin) in [#3055](https://github.com/jlowin/fastmcp/pull/3055)
* feat(context): Add background task support for Context (SEP-1686) by [@gfortaine](https://github.com/gfortaine) in [#2905](https://github.com/jlowin/fastmcp/pull/2905)
* Add server version to banner by [@richardkmichael](https://github.com/richardkmichael) in [#3076](https://github.com/jlowin/fastmcp/pull/3076)
* Add @handle_tool_errors decorator for standardized error handling by [@dgenio](https://github.com/dgenio) in [#2885](https://github.com/jlowin/fastmcp/pull/2885)
* Update Anthropic and OpenAI clients to use Omit instead of NotGiven by [@jlowin](https://github.com/jlowin) in [#3088](https://github.com/jlowin/fastmcp/pull/3088)
* Add ResponseLimitingMiddleware for tool response size control by [@dgenio](https://github.com/dgenio) in [#3072](https://github.com/jlowin/fastmcp/pull/3072)
* Infer MIME types from OpenAPI response definitions by [@jlowin](https://github.com/jlowin) in [#3101](https://github.com/jlowin/fastmcp/pull/3101)
* Remove require_auth in favor of scope-based authorization by [@jlowin](https://github.com/jlowin) in [#3103](https://github.com/jlowin/fastmcp/pull/3103)
### Fixes 🐞
* Fix FastAPI mounting examples in docs by [@jlowin](https://github.com/jlowin) in [#2962](https://github.com/jlowin/fastmcp/pull/2962)
* Remove outdated 'FastMCP 3.0 is coming!' CLI banner by [@jlowin](https://github.com/jlowin) in [#2974](https://github.com/jlowin/fastmcp/pull/2974)
* Pin httpx `< 1.0` and simplify beta install docs by [@jlowin](https://github.com/jlowin) in [#2975](https://github.com/jlowin/fastmcp/pull/2975)
* Add enabled field to ToolTransformConfig by [@jlowin](https://github.com/jlowin) in [#2991](https://github.com/jlowin/fastmcp/pull/2991)
* fix phue2 import in smart_home example by [@zzstoatzz](https://github.com/zzstoatzz) in [#2999](https://github.com/jlowin/fastmcp/pull/2999)
* fix: broaden combine_lifespans type to accept Mapping return types by [@aminsamir45](https://github.com/aminsamir45) in [#3005](https://github.com/jlowin/fastmcp/pull/3005)
* fix: type narrowing for skills resource contents by [@strawgate](https://github.com/strawgate) in [#3023](https://github.com/jlowin/fastmcp/pull/3023)
* fix: correctly send resource when exchanging code for the upstream by [@JonasKs](https://github.com/JonasKs) in [#3013](https://github.com/jlowin/fastmcp/pull/3013)
* MCP Apps: structured CSP/permissions types, resource meta propagation fix, QR example by [@jlowin](https://github.com/jlowin) in [#3031](https://github.com/jlowin/fastmcp/pull/3031)
* chore: upgrade python-multipart to 0.0.22 (CVE-2026-24486) by [@jlowin](https://github.com/jlowin) in [#3042](https://github.com/jlowin/fastmcp/pull/3042)
* chore: upgrade protobuf to 6.33.5 (CVE-2026-0994) by [@jlowin](https://github.com/jlowin) in [#3043](https://github.com/jlowin/fastmcp/pull/3043)
* fix: use MCP spec error code -32002 for resource not found by [@jlowin](https://github.com/jlowin) in [#3041](https://github.com/jlowin/fastmcp/pull/3041)
* Fix tool_choice reset for structured output sampling by [@strawgate](https://github.com/strawgate) in [#3014](https://github.com/jlowin/fastmcp/pull/3014)
* Fix workflow notification URL formatting in upgrade checks by [@strawgate](https://github.com/strawgate) in [#3047](https://github.com/jlowin/fastmcp/pull/3047)
* Fix Field() handling in prompts by [@strawgate](https://github.com/strawgate) in [#3050](https://github.com/jlowin/fastmcp/pull/3050)
* fix: use SkipJsonSchema to exclude callable fields from JSON schema generation by [@strawgate](https://github.com/strawgate) in [#3048](https://github.com/jlowin/fastmcp/pull/3048)
* fix: Preserve metadata in FastMCPProvider component wrappers by [@NeelayS](https://github.com/NeelayS) in [#3057](https://github.com/jlowin/fastmcp/pull/3057)
* Mock network calls in CLI tests and use MemoryStore for OAuth tests by [@strawgate](https://github.com/strawgate) in [#3051](https://github.com/jlowin/fastmcp/pull/3051)
* Remove OpenAPI timeout parameter, make client optional, surface timeout errors by [@jlowin](https://github.com/jlowin) in [#3067](https://github.com/jlowin/fastmcp/pull/3067)
* fix: enforce redirect URI validation when allowed_client_redirect_uris is supplied by [@nathanwelsh8](https://github.com/nathanwelsh8) in [#3066](https://github.com/jlowin/fastmcp/pull/3066)
* Fix --reload port conflict when using explicit port by [@jlowin](https://github.com/jlowin) in [#3070](https://github.com/jlowin/fastmcp/pull/3070)
* Fix compress_schema to preserve additionalProperties: false for MCP compatibility by [@jlowin](https://github.com/jlowin) in [#3102](https://github.com/jlowin/fastmcp/pull/3102)
* Fix CIMD redirect allowlist bypass and cache revalidation by [@jlowin](https://github.com/jlowin) in [#3098](https://github.com/jlowin/fastmcp/pull/3098)
* Exclude content-type from get_http_headers() to prevent HTTP 415 errors by [@jlowin](https://github.com/jlowin) in [#3104](https://github.com/jlowin/fastmcp/pull/3104)
### Docs 📚
* Prepare docs for v3.0 beta release by [@jlowin](https://github.com/jlowin) in [#2954](https://github.com/jlowin/fastmcp/pull/2954)
* Restructure docs: move transforms to dedicated section by [@jlowin](https://github.com/jlowin) in [#2956](https://github.com/jlowin/fastmcp/pull/2956)
* Remove unnecessary pip warning by [@jlowin](https://github.com/jlowin) in [#2958](https://github.com/jlowin/fastmcp/pull/2958)
* Update example MCP version in installation docs by [@jlowin](https://github.com/jlowin) in [#2959](https://github.com/jlowin/fastmcp/pull/2959)
* Update brand images by [@jlowin](https://github.com/jlowin) in [#2960](https://github.com/jlowin/fastmcp/pull/2960)
* Restructure README and welcome page with motivated narrative by [@jlowin](https://github.com/jlowin) in [#2963](https://github.com/jlowin/fastmcp/pull/2963)
* Restructure README and docs with motivated narrative by [@jlowin](https://github.com/jlowin) in [#2964](https://github.com/jlowin/fastmcp/pull/2964)
* Favicon update and Prefect Horizon docs by [@jlowin](https://github.com/jlowin) in [#2978](https://github.com/jlowin/fastmcp/pull/2978)
* Add dependency injection documentation and DI-style dependencies by [@jlowin](https://github.com/jlowin) in [#2980](https://github.com/jlowin/fastmcp/pull/2980)
* docs: document expanded reload behavior and restructure beta sections by [@jlowin](https://github.com/jlowin) in [#3039](https://github.com/jlowin/fastmcp/pull/3039)
* Add output_schema caveat to response limiting docs by [@jlowin](https://github.com/jlowin) in [#3099](https://github.com/jlowin/fastmcp/pull/3099)
* Document token passthrough security in OAuth Proxy docs by [@jlowin](https://github.com/jlowin) in [#3100](https://github.com/jlowin/fastmcp/pull/3100)
### Dependencies 📦
* Bump ty from 0.0.12 to 0.0.13 by @dependabot in [#2984](https://github.com/jlowin/fastmcp/pull/2984)
* Bump prek from 0.2.30 to 0.3.0 by @dependabot in [#2982](https://github.com/jlowin/fastmcp/pull/2982)
### Other Changes 🦾
* Normalize resource URLs before comparison to support RFC 8707 query parameters by [@abhijeethp](https://github.com/abhijeethp) in [#2967](https://github.com/jlowin/fastmcp/pull/2967)
* Bump pydocket to 0.17.2 (memory leak fix) by [@chrisguidry](https://github.com/chrisguidry) in [#2998](https://github.com/jlowin/fastmcp/pull/2998)
* Add AzureJWTVerifier for Managed Identity token verification by [@jlowin](https://github.com/jlowin) in [#3058](https://github.com/jlowin/fastmcp/pull/3058)
* Add release notes for v2.14.4 and v2.14.5 by [@jlowin](https://github.com/jlowin) in [#3064](https://github.com/jlowin/fastmcp/pull/3064)
* Add missing beta2 features to v3 release tracking by [@jlowin](https://github.com/jlowin) in [#3105](https://github.com/jlowin/fastmcp/pull/3105)
## New Contributors
* [@abhijeethp](https://github.com/abhijeethp) made their first contribution in [#2967](https://github.com/jlowin/fastmcp/pull/2967)
* [@aminsamir45](https://github.com/aminsamir45) made their first contribution in [#3005](https://github.com/jlowin/fastmcp/pull/3005)
* [@JonasKs](https://github.com/JonasKs) made their first contribution in [#2997](https://github.com/jlowin/fastmcp/pull/2997)
* [@NeelayS](https://github.com/NeelayS) made their first contribution in [#3057](https://github.com/jlowin/fastmcp/pull/3057)
* [@gfortaine](https://github.com/gfortaine) made their first contribution in [#2905](https://github.com/jlowin/fastmcp/pull/2905)
* [@nathanwelsh8](https://github.com/nathanwelsh8) made their first contribution in [#3066](https://github.com/jlowin/fastmcp/pull/3066)
* [@dgenio](https://github.com/dgenio) made their first contribution in [#2885](https://github.com/jlowin/fastmcp/pull/2885)
**Full Changelog**: https://github.com/jlowin/fastmcp/compare/v3.0.0b1...v3.0.0b2
</Update>
<Update label="v3.0.0b1" description="2026-01-20">
**[v3.0.0b1: This Beta Work](https://github.com/jlowin/fastmcp/releases/tag/v3.0.0b1)**

View file

@ -55,6 +55,8 @@ You don't need to pass `mcp_url` when using `OAuth` with `Client(auth=...)` —
- **`scopes`** (`str | list[str]`, optional): OAuth scopes to request. Can be space-separated string or list of strings
- **`client_name`** (`str`, optional): Client name for dynamic registration. Defaults to `"FastMCP Client"`
- **`client_id`** (`str`, optional): Pre-registered OAuth client ID. When provided, skips Dynamic Client Registration entirely. See [Pre-Registered Clients](#pre-registered-clients)
- **`client_secret`** (`str`, optional): OAuth client secret for pre-registered clients. Optional — public clients that rely on PKCE can omit this
- **`client_metadata_url`** (`str`, optional): URL-based client identity (CIMD). See [CIMD Authentication](/clients/auth/cimd) for details
- **`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
@ -74,7 +76,7 @@ The client first checks the configured `token_storage` backend for existing, val
If no valid tokens exist, the client attempts to discover the OAuth server's endpoints using a well-known URI (e.g., `/.well-known/oauth-authorization-server`) based on the `mcp_url`.
</Step>
<Step title="Client Registration">
If the OAuth server supports it and the client isn't already registered (or credentials aren't cached), the client performs dynamic client registration according to RFC 7591. Alternatively, if a `client_metadata_url` is configured and the server supports CIMD, the client uses its metadata URL as its identity instead of registering.
If a `client_id` is provided, the client uses those pre-registered credentials directly and skips this step entirely. Otherwise, if a `client_metadata_url` is configured and the server supports CIMD, the client uses its metadata URL as its identity. As a fallback, the client performs Dynamic Client Registration (RFC 7591) if the server supports it.
</Step>
<Step title="Local Callback Server">
A temporary local HTTP server is started on an available port (or the port specified via `callback_port`). This server's address (e.g., `http://127.0.0.1:<port>/callback`) acts as the `redirect_uri` for the OAuth flow.
@ -152,3 +154,33 @@ async with Client(
```
See the [CIMD Authentication](/clients/auth/cimd) page for complete documentation on creating, hosting, and validating CIMD documents.
## Pre-Registered Clients
<VersionBadge version="3.0.0" />
Some OAuth servers don't support Dynamic Client Registration — the MCP spec explicitly makes DCR optional. If your client has been pre-registered with the server (you already have a `client_id` and optionally a `client_secret`), you can provide them directly to skip DCR entirely.
```python
from fastmcp import Client
from fastmcp.client.auth import OAuth
async with Client(
"https://mcp-server.example.com/mcp",
auth=OAuth(
client_id="my-registered-client-id",
client_secret="my-client-secret",
),
) as client:
await client.ping()
```
Public clients that rely on PKCE for security can omit `client_secret`:
```python
oauth = OAuth(client_id="my-public-client-id")
```
<Note>
When using pre-registered credentials, the client will not attempt Dynamic Client Registration. If the server rejects the credentials, the error is surfaced immediately rather than falling back to DCR.
</Note>

View file

@ -43,9 +43,9 @@ tool.disable()
server.disable(names={"my_tool"}, components=["tool"])
```
#### Listing Methods Return Lists
#### Listing Methods Renamed and Return Lists
`get_tools()`, `get_resources()`, `get_prompts()`, and `get_resource_templates()` now return lists instead of dicts:
`get_tools()`, `get_resources()`, `get_prompts()`, and `get_resource_templates()` have been replaced by `list_tools()`, `list_resources()`, `list_prompts()`, and `list_resource_templates()`. The new methods return lists instead of dicts:
```python
# Before
@ -53,7 +53,7 @@ tools = await server.get_tools()
tool = tools["my_tool"]
# After
tools = await server.get_tools()
tools = await server.list_tools()
tool = next((t for t in tools if t.name == "my_tool"), None)
```
@ -91,6 +91,20 @@ await ctx.set_state("key", "value")
value = await ctx.get_state("key")
```
#### State Values Must Be Serializable
Session state values must now be JSON-serializable by default (dicts, lists, strings, numbers, etc.), since state is persisted across requests using a pluggable storage backend.
If you need to store non-serializable values (e.g., passing an HTTP client from middleware to a tool), use `serializable=False`. These values are request-scoped and only available during the current tool call, resource read, or prompt render:
```python
# Middleware sets up a client for the current request
await ctx.set_state("client", my_http_client, serializable=False)
# Tool retrieves it in the same request
client = await ctx.get_state("client")
```
#### Server Banner Environment Variable Renamed
`FASTMCP_SHOW_CLI_BANNER` is now `FASTMCP_SHOW_SERVER_BANNER`.

View file

@ -4,6 +4,128 @@ title: v3.0 Feature Tracking
This document tracks major features in FastMCP v3.0 for release notes preparation.
## 3.0.0rc1
### SamplingTool Conversion Helpers
Server tools (FunctionTool and TransformedTool) can now be passed directly to sampling methods via `SamplingTool.from_callable_tool()` ([#3062](https://github.com/jlowin/fastmcp/pull/3062)). Previously, tools defined with `@mcp.tool` had to be recreated as functions for use in `ctx.sample()`. Now `ctx.sample()` and `ctx.sample_step()` accept these tool instances directly.
```python
@mcp.tool
def search(query: str) -> str:
"""Search the web."""
return do_search(query)
# Use tool directly in sampling
result = await ctx.sample(
"Research Python frameworks",
tools=[search] # FunctionTool works directly!
)
```
### Concurrent Tool Execution in Sampling
When an LLM returns multiple tool calls in a single sampling response, they can now be executed concurrently ([#3022](https://github.com/jlowin/fastmcp/pull/3022)). Default behavior remains sequential; opt in with `tool_concurrency`. Tools can declare `sequential=True` to force sequential execution even when concurrency is enabled.
```python
result = await context.sample(
messages="Fetch weather for NYC and LA",
tools=[fetch_weather],
tool_concurrency=0, # Unlimited parallel execution
)
```
### OpenAPI `validate_output` Option
`OpenAPIProvider` and `FastMCP.from_openapi()` now accept `validate_output=False` to skip output schema validation ([#3134](https://github.com/jlowin/fastmcp/pull/3134)). Useful when backends don't conform to their own OpenAPI response schemas — structured JSON still flows through, only the strict schema checking is disabled.
```python
mcp = FastMCP.from_openapi(
openapi_spec=spec,
client=client,
validate_output=False,
)
```
### Auth Token Injection and Azure OBO Dependencies
New dependency injection for accessing the authenticated user's token directly in tool parameters ([#2918](https://github.com/jlowin/fastmcp/pull/2918)). Works with any auth provider.
```python
from fastmcp.server.dependencies import CurrentAccessToken, TokenClaim
from fastmcp.server.auth import AccessToken
@mcp.tool()
async def my_tool(
token: AccessToken = CurrentAccessToken,
user_id: str = TokenClaim("oid"),
): ...
```
For Azure/Entra, the new `fastmcp[azure]` extra adds `EntraOBOToken`, which handles the On-Behalf-Of token exchange declaratively:
```python
from fastmcp.server.auth.providers.azure import EntraOBOToken
@mcp.tool()
async def get_emails(
graph_token: str = EntraOBOToken(["https://graph.microsoft.com/Mail.Read"]),
):
# graph_token is ready — OBO exchange happened automatically
...
```
### `generate-cli` Agent Skill Generation
`fastmcp generate-cli` now produces a `SKILL.md` alongside the CLI script ([#3115](https://github.com/jlowin/fastmcp/pull/3115)) — a Claude Code agent skill with pre-computed invocation syntax for every tool. Agents reading the skill can call tools immediately without running `--help`. On by default; pass `--no-skill` to opt out.
### Background Task Notification Queue
Background tasks now use a distributed Redis notification queue for reliable delivery ([#2906](https://github.com/jlowin/fastmcp/pull/2906)). Elicitation switches from polling to BLPOP (single blocking call instead of ~7,200 round-trips/hour), and notification delivery retries up to 3x with TTL-based expiration.
### Async Auth Checks
Auth check functions can now be `async`, enabling authorization decisions that depend on asynchronous operations like reading server state via `Context.get_state` or calling external services ([#3150](https://github.com/jlowin/fastmcp/issues/3150)). Sync and async checks can be freely mixed. Previously, passing an async function as an auth check would silently pass (coroutine objects are truthy).
### Optional `$ref` Dereferencing in Schemas
Schema `$ref` dereferencing — which inlines all `$defs` for compatibility with MCP clients that don't handle `$ref` — is now controlled by the `dereference_schemas` constructor kwarg ([#3141](https://github.com/jlowin/fastmcp/issues/3141)). Default is `True` (dereference on) because the non-compliant clients are popular and the failure mode is silent breakage that server authors can't diagnose. Opt out when you know your clients handle `$ref` and want smaller schemas:
```python
mcp = FastMCP("my-server", dereference_schemas=False)
```
Dereferencing is implemented as middleware (`DereferenceRefsMiddleware`) that runs at serve-time, so schemas are stored with `$ref` intact and only inlined when sent to clients.
### Breaking: Deprecated `FastMCP()` Constructor Kwargs Removed
Sixteen deprecated keyword arguments have been removed from `FastMCP.__init__`. Passing any of them now raises `TypeError` with a migration hint. Environment variables (e.g., `FASTMCP_HOST`) continue to work — only the constructor kwargs moved.
**Transport/server settings** (`host`, `port`, `log_level`, `debug`, `sse_path`, `message_path`, `streamable_http_path`, `json_response`, `stateless_http`): Pass to `run()`, `run_http_async()`, or `http_app()` as appropriate, or set via environment variables.
```python
# Before
mcp = FastMCP("server", host="0.0.0.0", port=8080)
mcp.run()
# After
mcp = FastMCP("server")
mcp.run(transport="http", host="0.0.0.0", port=8080)
```
**Duplicate handling** (`on_duplicate_tools`, `on_duplicate_resources`, `on_duplicate_prompts`): Use the unified `on_duplicate=` parameter.
**Tag filtering** (`include_tags`, `exclude_tags`): Use `server.enable(tags=..., only=True)` and `server.disable(tags=...)` after construction.
**Tool serializer** (`tool_serializer`): Return `ToolResult` from tools instead.
**Tool transformations** (`tool_transformations`): Use `server.add_transform(ToolTransform(...))` after construction.
The `_deprecated_settings` attribute and `.settings` property are also removed. `ExperimentalSettings` has been deleted (dead code).
### Breaking: `ui=` Renamed to `app=`
The MCP Apps decorator parameter has been renamed from `ui=ToolUI(...)` / `ui=ResourceUI(...)` to `app=AppConfig(...)` ([#3117](https://github.com/jlowin/fastmcp/pull/3117)). `ToolUI` and `ResourceUI` are consolidated into a single `AppConfig` class. Wire format is unchanged. See the MCP Apps section under beta2 for full details.
## 3.0.0beta2
### CLI: `fastmcp list` and `fastmcp call`
@ -120,6 +242,29 @@ Key details:
Documentation: [CIMD Authentication](/clients/auth/cimd), [OAuth Proxy CIMD config](/servers/auth/oauth-proxy#cimd-support)
### Pre-Registered OAuth Clients
The `OAuth` client helper now accepts `client_id` and `client_secret` parameters for servers where the client is already registered ([#3086](https://github.com/jlowin/fastmcp/pull/3086)). This bypasses Dynamic Client Registration entirely — useful when DCR is disabled, or when the server has pre-provisioned credentials for your application.
```python
from fastmcp import Client
from fastmcp.client.auth import OAuth
async with Client(
"https://mcp-server.example.com/mcp",
auth=OAuth(
client_id="my-registered-app",
client_secret="my-secret",
scopes=["read", "write"],
),
) as client:
await client.ping()
```
The static credentials are injected before the OAuth flow begins, so the client never attempts DCR. If the server rejects the credentials, the error surfaces immediately rather than retrying with fresh registration (which can't help for fixed credentials). Public clients can omit `client_secret`.
Documentation: [Pre-Registered Clients](/clients/auth/oauth#pre-registered-clients)
### CLI: `fastmcp generate-cli`
`fastmcp generate-cli` connects to any MCP server, reads its tool schemas, and writes a standalone Python CLI script where every tool becomes a typed subcommand with flags, help text, and tab completion ([#3065](https://github.com/jlowin/fastmcp/pull/3065)). The insight is that MCP tool schemas already contain everything a CLI framework needs — parameter names, types, descriptions, required/optional status — so the generator maps JSON Schema directly into [cyclopts](https://cyclopts.readthedocs.io/) commands.
@ -1177,35 +1322,9 @@ main.mount(subserver, prefix="api")
main.mount(subserver, namespace="api")
```
#### Tag Filtering Init Parameters
#### Tag Filtering, Tool Serializer, Tool Transformations Init Parameters
`FastMCP(include_tags=..., exclude_tags=...)` deprecated. Use `enable()`/`disable()` methods:
```python
# Deprecated
mcp = FastMCP("server", exclude_tags={"internal"})
# New
mcp = FastMCP("server")
mcp.disable(tags={"internal"})
```
#### Tool Serializer Parameter
The `tool_serializer` parameter on `FastMCP` is deprecated. Return `ToolResult` for explicit serialization control.
#### Tool Transformation Methods
`add_tool_transformation()`, `remove_tool_transformation()`, and `tool_transformations` constructor parameter are deprecated. Use `add_transform(ToolTransform({...}))` instead:
```python
# Deprecated
mcp.add_tool_transformation("name", config)
# New
from fastmcp.server.transforms import ToolTransform
mcp.add_transform(ToolTransform({"name": config}))
```
These constructor parameters have been **removed** (not just deprecated) as of rc1. See "Breaking: Deprecated `FastMCP()` Constructor Kwargs Removed" in the rc1 section above. The `add_tool_transformation()` and `remove_tool_transformation()` methods remain as deprecated shims.
---
@ -1257,7 +1376,8 @@ server.disable(names={"my_tool"}, components=["tool"])
Server lookup and listing methods have updated signatures:
- Parameter names: `get_tool(name=...)`, `get_resource(uri=...)`, etc. (was `key`)
- Return types: `get_tools()`, `get_resources()`, etc. return lists instead of dicts
- Plural listing methods renamed: `get_tools()` → `list_tools()`, `get_resources()` → `list_resources()`, etc.
- Return types: `list_tools()`, `list_resources()`, etc. return lists instead of dicts
```python
# v2.x
@ -1265,7 +1385,7 @@ tools = await server.get_tools()
tool = tools["my_tool"]
# v3.0
tools = await server.get_tools()
tools = await server.list_tools()
tool = next((t for t in tools if t.name == "my_tool"), None)
```

View file

@ -467,6 +467,7 @@
"python-sdk/fastmcp-server-middleware-__init__",
"python-sdk/fastmcp-server-middleware-authorization",
"python-sdk/fastmcp-server-middleware-caching",
"python-sdk/fastmcp-server-middleware-dereference",
"python-sdk/fastmcp-server-middleware-error_handling",
"python-sdk/fastmcp-server-middleware-logging",
"python-sdk/fastmcp-server-middleware-middleware",
@ -562,6 +563,7 @@
"python-sdk/fastmcp-server-tasks-elicitation",
"python-sdk/fastmcp-server-tasks-handlers",
"python-sdk/fastmcp-server-tasks-keys",
"python-sdk/fastmcp-server-tasks-notifications",
"python-sdk/fastmcp-server-tasks-requests",
"python-sdk/fastmcp-server-tasks-routing",
"python-sdk/fastmcp-server-tasks-subscriptions"
@ -665,7 +667,7 @@
"icon": "code"
}
],
"version": "v3.0.0 (beta 2)"
"version": "v3.0.0 (rc 1)"
},
{
"dropdowns": [
@ -862,7 +864,7 @@
"icon": "book"
}
],
"version": "v2.14.3"
"version": "v2.14.5"
}
]
},

View file

@ -8,17 +8,17 @@ icon: arrow-down-to-line
We recommend using [uv](https://docs.astral.sh/uv/getting-started/installation/) to install and manage FastMCP.
<Note>
FastMCP 3.0 is currently in beta. Package managers won't install beta versions by default—you must explicitly request one (e.g., `>=3.0.0b2`).
FastMCP 3.0 is currently a release candidate. Package managers won't install pre-release versions by default—you must explicitly request one (e.g., `>=3.0.0rc1`).
</Note>
```bash
pip install "fastmcp>=3.0.0b2"
pip install "fastmcp>=3.0.0rc1"
```
Or with uv:
```bash
uv add "fastmcp>=3.0.0b2"
uv add "fastmcp>=3.0.0rc1"
```
### Optional Dependencies
@ -26,7 +26,7 @@ uv add "fastmcp>=3.0.0b2"
FastMCP provides optional extras for specific features. For example, to install the background tasks extra:
```bash
pip install "fastmcp[tasks]==3.0.0b2"
pip install "fastmcp[tasks]==3.0.0rc1"
```
See [Background Tasks](/servers/tasks) for details on the task system.
@ -44,7 +44,7 @@ You should see output like the following:
```bash
$ fastmcp version
FastMCP version: 3.0.0
FastMCP version: 3.0.0rc1
MCP version: 1.25.0
Python version: 3.12.2
Platform: macOS-15.3.1-arm64-arm-64bit

View file

@ -36,7 +36,7 @@ if __name__ == "__main__":
```
<Tip>
**This documentation is for FastMCP 3.0**, which is currently in beta. For the 2.x release, see the [FastMCP 2.0 documentation](/v2/getting-started/welcome).
**This documentation is for FastMCP 3.0**, which is currently a release candidate. For the 2.x release, see the [FastMCP 2.0 documentation](/v2/getting-started/welcome).
</Tip>
FastMCP is made with 💙 by [Prefect](https://www.prefect.io/).

View file

@ -326,3 +326,135 @@ mcp = FastMCP(name="Azure MI App", auth=auth)
<Note>
For Azure Government, pass `base_authority="login.microsoftonline.us"` to `AzureJWTVerifier`.
</Note>
## On-Behalf-Of (OBO)
<VersionBadge version="3.0.0" />
The On-Behalf-Of (OBO) flow allows your FastMCP server to call downstream Microsoft APIs—like Microsoft Graph—using the authenticated user's identity. When a user authenticates to your MCP server, you receive a token for your API. OBO exchanges that token for a new token that can call other services, maintaining the user's identity and permissions throughout the chain.
This pattern is useful when your tools need to access user-specific data from Microsoft services: reading emails, accessing calendar events, querying SharePoint, or any other Graph API operation that requires user context.
<Note>
OBO features require the `azure` extra:
```bash
pip install 'fastmcp[azure]'
```
</Note>
### Azure Portal Setup
OBO requires additional configuration in your Azure App registration beyond basic authentication.
<Steps>
<Step title="Add API Permissions">
In your App registration, navigate to **API permissions** and add the Microsoft Graph permissions your tools will need.
- Click **Add a permission** → **Microsoft Graph** → **Delegated permissions**
- Select the permissions required for your use case (e.g., `Mail.Read`, `Calendars.Read`, `User.Read`)
- Repeat for any other APIs you need to call
<Warning>
Only add delegated permissions for OBO. Application permissions bypass user context entirely and are inappropriate for the OBO flow.
</Warning>
</Step>
<Step title="Grant Admin Consent">
OBO requires admin consent for the permissions you've added. In the **API permissions** page, click **Grant admin consent for [Your Organization]**.
Without admin consent, OBO token exchanges will fail with an `AADSTS65001` error indicating the user or administrator hasn't consented to use the application.
<Tip>
For development, you can grant consent for just your own account. For production, an Azure AD administrator must grant tenant-wide consent.
</Tip>
</Step>
</Steps>
### Configure AzureProvider for OBO
The `additional_authorize_scopes` parameter tells Azure which downstream API permissions to include during the initial authorization. These scopes establish what your server can request through OBO later.
```python server.py
from fastmcp import FastMCP
from fastmcp.server.auth.providers.azure import AzureProvider
auth_provider = AzureProvider(
client_id="your-client-id",
client_secret="your-client-secret",
tenant_id="your-tenant-id",
base_url="http://localhost:8000",
required_scopes=["mcp-access"], # Your API scope
# Include Graph scopes for OBO
additional_authorize_scopes=[
"https://graph.microsoft.com/Mail.Read",
"https://graph.microsoft.com/User.Read",
"offline_access", # Enables refresh tokens
],
)
mcp = FastMCP(name="Graph-Enabled Server", auth=auth_provider)
```
Scopes listed in `additional_authorize_scopes` are requested during the initial OAuth flow but aren't validated on incoming tokens. They establish permission for your server to later exchange the user's token for downstream API access.
<Info>
Use fully-qualified scope URIs for downstream APIs (e.g., `https://graph.microsoft.com/Mail.Read`). Short forms like `Mail.Read` work for authorization requests, but fully-qualified URIs are clearer and avoid ambiguity.
</Info>
### EntraOBOToken Dependency
The `EntraOBOToken` dependency handles the complete OBO flow automatically. Declare it as a parameter default with the scopes you need, and FastMCP exchanges the user's token for a downstream API token before your function runs.
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.azure import AzureProvider, EntraOBOToken
import httpx
auth_provider = AzureProvider(
client_id="your-client-id",
client_secret="your-client-secret",
tenant_id="your-tenant-id",
base_url="http://localhost:8000",
required_scopes=["mcp-access"],
additional_authorize_scopes=[
"https://graph.microsoft.com/Mail.Read",
"https://graph.microsoft.com/User.Read",
],
)
mcp = FastMCP(name="Email Reader", auth=auth_provider)
@mcp.tool
async def get_recent_emails(
count: int = 10,
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:
response = await client.get(
f"https://graph.microsoft.com/v1.0/me/messages?$top={count}",
headers={"Authorization": f"Bearer {graph_token}"},
)
response.raise_for_status()
data = response.json()
return [
{"subject": msg["subject"], "from": msg["from"]["emailAddress"]["address"]}
for msg in data.get("value", [])
]
```
The `graph_token` parameter receives a ready-to-use access token for Microsoft Graph. FastMCP handles the OBO exchange transparently—your function just uses the token to call the API.
<Warning>
**Scope alignment is critical.** The scopes passed to `EntraOBOToken` must be a subset of the scopes in `additional_authorize_scopes`. If you request a scope during OBO that wasn't included in the initial authorization, the exchange will fail.
</Warning>
<Tip>
For advanced OBO scenarios, use `CurrentAccessToken()` to get the user's token, then construct an `azure.identity.aio.OnBehalfOfCredential` directly with your Azure credentials.
</Tip>
<Tip>
For a complete working example of Azure OBO with FastMCP, see [Pamela Fox's blog post on OBO flow for Entra-based MCP servers](https://blog.pamelafox.org/2026/01/using-on-behalf-of-flow-for-entra-based.html).
</Tip>

View file

@ -6,7 +6,7 @@ sidebarTitle: generate
# `fastmcp.cli.generate`
Generate a standalone CLI script from an MCP server's capabilities.
Generate a standalone CLI script and agent skill from an MCP server.
## Functions
@ -33,7 +33,17 @@ generate_cli_script(server_name: str, server_spec: str, transport_code: str, ext
Generate the full CLI script source code.
### `generate_cli_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/generate.py#L526" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `generate_skill_content` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/generate.py#L621" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
generate_skill_content(server_name: str, cli_filename: str, tools: list[mcp.types.Tool]) -> str
```
Generate a SKILL.md file for a generated CLI script.
### `generate_cli_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/generate.py#L672" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
generate_cli_command(server_spec: Annotated[str, cyclopts.Parameter(help='Server URL, Python file, MCPConfig JSON, discovered name, or .js file')], output: Annotated[str, cyclopts.Parameter(help='Output file path (default: cli.py)')] = 'cli.py') -> None
@ -43,7 +53,8 @@ generate_cli_command(server_spec: Annotated[str, cyclopts.Parameter(help='Server
Generate a standalone CLI script from an MCP server.
Connects to the server, reads its tools/resources/prompts, and writes
a Python script that can invoke them directly.
a Python script that can invoke them directly. Also generates a SKILL.md
agent skill file unless --no-skill is passed.
**Examples:**
@ -51,4 +62,5 @@ fastmcp generate-cli weather
fastmcp generate-cli weather my_cli.py
fastmcp generate-cli http://localhost:8000/mcp
fastmcp generate-cli server.py output.py -f
fastmcp generate-cli weather --no-skill

View file

@ -73,7 +73,7 @@ a browser for user authorization and running a local callback server.
**Methods:**
#### `redirect_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L259" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `redirect_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L290" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
redirect_handler(self, authorization_url: str) -> None
@ -82,7 +82,7 @@ redirect_handler(self, authorization_url: str) -> None
Open browser for authorization, with pre-flight check for invalid client.
#### `callback_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L280" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `callback_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L311" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
callback_handler(self) -> tuple[str, str | None]
@ -91,7 +91,7 @@ callback_handler(self) -> tuple[str, str | None]
Handle OAuth callback and return (auth_code, state).
#### `async_auth_flow` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L319" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `async_auth_flow` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L350" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]

View file

@ -58,7 +58,7 @@ large result sets incrementally), use list_prompts_mcp() with the cursor paramet
- `McpError`: If the request results in a TimeoutError | JSONRPCError
#### `get_prompt_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/prompts.py#L84" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_prompt_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/prompts.py#L92" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompt_mcp(self: Client, name: str, arguments: dict[str, Any] | None = None, meta: dict[str, Any] | None = None) -> mcp.types.GetPromptResult
@ -80,19 +80,19 @@ containing the prompt messages and any additional metadata.
- `McpError`: If the request results in a TimeoutError | JSONRPCError
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/prompts.py#L153" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/prompts.py#L161" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompt(self: Client, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult
```
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/prompts.py#L164" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/prompts.py#L172" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompt(self: Client, name: str, arguments: dict[str, Any] | None = None) -> PromptTask
```
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/prompts.py#L176" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/prompts.py#L184" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompt(self: Client, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult | PromptTask

View file

@ -58,7 +58,7 @@ large result sets incrementally), use list_resources_mcp() with the cursor param
- `McpError`: If the request results in a TimeoutError | JSONRPCError
#### `list_resource_templates_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L82" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_resource_templates_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L90" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resource_templates_mcp(self: Client) -> mcp.types.ListResourceTemplatesResult
@ -78,7 +78,7 @@ containing the list of resource templates and any additional metadata.
- `McpError`: If the request results in a TimeoutError | JSONRPCError
#### `list_resource_templates` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L105" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_resource_templates` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L113" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resource_templates(self: Client) -> list[mcp.types.ResourceTemplate]
@ -99,7 +99,7 @@ cursor parameter.
- `McpError`: If the request results in a TimeoutError | JSONRPCError
#### `read_resource_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L132" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read_resource_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L149" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource_mcp(self: Client, uri: AnyUrl | str, meta: dict[str, Any] | None = None) -> mcp.types.ReadResourceResult
@ -120,19 +120,19 @@ containing the resource contents and any additional metadata.
- `McpError`: If the request results in a TimeoutError | JSONRPCError
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L188" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L205" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource(self: Client, uri: AnyUrl | str) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]
```
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L198" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L215" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource(self: Client, uri: AnyUrl | str) -> ResourceTask
```
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L209" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/resources.py#L226" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource(self: Client, uri: AnyUrl | str) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents] | ResourceTask

View file

@ -58,7 +58,7 @@ large result sets incrementally), use list_tools_mcp() with the cursor parameter
- `McpError`: If the request results in a TimeoutError | JSONRPCError
#### `call_tool_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/tools.py#L88" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `call_tool_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/tools.py#L96" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
call_tool_mcp(self: Client, name: str, arguments: dict[str, Any], progress_handler: ProgressHandler | None = None, timeout: datetime.timedelta | float | int | None = None, meta: dict[str, Any] | None = None) -> mcp.types.CallToolResult
@ -88,19 +88,19 @@ containing the tool result and any additional metadata.
- `McpError`: If the tool call requests results in a TimeoutError | JSONRPCError
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/tools.py#L168" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/tools.py#L176" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
call_tool(self: Client, name: str, arguments: dict[str, Any] | None = None) -> CallToolResult
```
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/tools.py#L182" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/tools.py#L190" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
call_tool(self: Client, name: str, arguments: dict[str, Any] | None = None) -> ToolTask
```
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/tools.py#L197" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/mixins/tools.py#L205" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
call_tool(self: Client, name: str, arguments: dict[str, Any] | None = None) -> CallToolResult | ToolTask

View file

@ -42,7 +42,7 @@ infer_transport_type_from_url(url: str | AnyUrl) -> Literal['http', 'sse']
Infer the appropriate transport type from the given URL.
### `update_config_file` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L321" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `update_config_file` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L345" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
update_config_file(file_path: Path, server_name: str, server_config: CanonicalMCPServerTypes) -> None
@ -57,7 +57,7 @@ worry about transforming server objects here.
## Classes
### `StdioMCPServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L131" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `StdioMCPServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L155" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
MCP server configuration for stdio transport.
@ -67,19 +67,19 @@ This is the canonical configuration format for MCP servers using stdio transport
**Methods:**
#### `to_transport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L164" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `to_transport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L188" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
to_transport(self) -> StdioTransport
```
### `TransformingStdioMCPServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L176" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `TransformingStdioMCPServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L200" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A Stdio server with tool transforms.
### `RemoteMCPServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L180" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `RemoteMCPServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L204" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
MCP server configuration for HTTP/SSE transport.
@ -89,19 +89,19 @@ This is the canonical configuration format for MCP servers using remote transpor
**Methods:**
#### `to_transport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L216" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `to_transport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L240" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
to_transport(self) -> StreamableHttpTransport | SSETransport
```
### `TransformingRemoteMCPServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L241" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `TransformingRemoteMCPServer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L265" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A Remote server with tool transforms.
### `MCPConfig` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L252" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `MCPConfig` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L276" 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/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L266" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `wrap_servers_at_root` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L290" 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/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L279" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L303" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_server(self, name: str, server: MCPServerTypes) -> None
@ -131,7 +131,7 @@ add_server(self, name: str, server: MCPServerTypes) -> None
Add or update a server in the configuration.
#### `from_dict` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L284" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_dict` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L308" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_dict(cls, config: dict[str, Any]) -> Self
@ -140,7 +140,7 @@ from_dict(cls, config: dict[str, Any]) -> Self
Parse MCP configuration from dictionary format.
#### `to_dict` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L288" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `to_dict` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L312" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
to_dict(self) -> dict[str, Any]
@ -149,7 +149,7 @@ to_dict(self) -> dict[str, Any]
Convert MCPConfig to dictionary format, preserving all fields.
#### `write_to_file` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L292" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `write_to_file` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L316" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
write_to_file(self, file_path: Path) -> None
@ -158,7 +158,7 @@ write_to_file(self, file_path: Path) -> None
Write configuration to JSON file.
#### `from_file` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L298" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_file` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L322" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_file(cls, file_path: Path) -> Self
@ -167,7 +167,7 @@ from_file(cls, file_path: Path) -> Self
Load configuration from JSON file.
### `CanonicalMCPConfig` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L306" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `CanonicalMCPConfig` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L330" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Canonical MCP configuration format.
@ -178,7 +178,7 @@ The format is designed to be client-agnostic and extensible for future use cases
**Methods:**
#### `add_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L316" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/mcp_config.py#L340" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_server(self, name: str, server: CanonicalMCPServerTypes) -> None

View file

@ -62,7 +62,7 @@ A template for dynamically creating resources.
#### `from_function` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/template.py#L130" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_function(fn: Callable[..., Any], uri_template: str, name: str | None = None, version: str | int | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, mime_type: str | None = None, tags: set[str] | None = None, annotations: Annotations | None = None, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, auth: AuthCheckCallable | list[AuthCheckCallable] | None = None) -> FunctionResourceTemplate
from_function(fn: Callable[..., Any], uri_template: str, name: str | None = None, version: str | int | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, mime_type: str | None = None, tags: set[str] | None = None, annotations: Annotations | None = None, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, auth: AuthCheck | list[AuthCheck] | None = None) -> FunctionResourceTemplate
```
#### `set_default_mime_type` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/template.py#L163" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
@ -237,7 +237,7 @@ FunctionResourceTemplate splats the params dict since .fn expects **kwargs.
#### `from_function` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/resources/template.py#L460" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_function(cls, fn: Callable[..., Any], uri_template: str, name: str | None = None, version: str | int | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, mime_type: str | None = None, tags: set[str] | None = None, annotations: Annotations | None = None, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, auth: AuthCheckCallable | list[AuthCheckCallable] | None = None) -> FunctionResourceTemplate
from_function(cls, fn: Callable[..., Any], uri_template: str, name: str | None = None, version: str | int | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, mime_type: str | None = None, tags: set[str] | None = None, annotations: Annotations | None = None, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, auth: AuthCheck | list[AuthCheck] | None = None) -> FunctionResourceTemplate
```
Create a template from a function.

View file

@ -15,17 +15,17 @@ UI metadata for clients that support interactive app rendering.
## Functions
### `ui_to_meta_dict` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/apps.py#L129" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `app_config_to_meta_dict` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/apps.py#L115" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
ui_to_meta_dict(ui: ToolUI | ResourceUI | dict[str, Any]) -> dict[str, Any]
app_config_to_meta_dict(app: AppConfig | dict[str, Any]) -> dict[str, Any]
```
Convert a UI model or dict to the wire-format dict for ``meta["ui"]``.
Convert an AppConfig or dict to the wire-format dict for ``meta["ui"]``.
### `resolve_ui_mime_type` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/apps.py#L136" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `resolve_ui_mime_type` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/apps.py#L122" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
resolve_ui_mime_type(uri: str, explicit_mime_type: str | None) -> str | None
@ -70,18 +70,17 @@ iframe. Hosts MAY honour these; apps should use JS feature detection
as a fallback.
### `ToolUI` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/apps.py#L77" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `AppConfig` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/apps.py#L77" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Typed ``_meta.ui`` for tools — links a tool to its UI resource.
Configuration for MCP App tools and resources.
Controls how a tool or resource participates in the MCP Apps extension.
On tools, ``resource_uri`` and ``visibility`` specify which UI resource
to render and where the tool appears. On resources, those fields must
be left unset (the resource itself is the UI).
All fields use ``exclude_none`` serialization so only explicitly-set
values appear on the wire. Aliases match the MCP Apps wire format
(camelCase).
### `ResourceUI` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/apps.py#L110" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Typed ``_meta.ui`` for resources — rendering hints for UI-capable clients.

View file

@ -36,7 +36,7 @@ Example:
## Functions
### `require_scopes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L77" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `require_scopes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L78" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
require_scopes(*scopes: str) -> AuthCheck
@ -52,7 +52,7 @@ in the token (AND logic).
- `*scopes`: One or more scope strings that must all be present.
### `restrict_tag` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L105" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `restrict_tag` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
restrict_tag(tag: str) -> AuthCheck
@ -69,7 +69,7 @@ required scopes. If the component doesn't have the tag, access is allowed.
- `scopes`: List of scopes required when the tag is present.
### `run_auth_checks` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L133" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `run_auth_checks` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L134" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run_auth_checks(checks: AuthCheck | list[AuthCheck], ctx: AuthContext) -> bool
@ -78,7 +78,8 @@ run_auth_checks(checks: AuthCheck | list[AuthCheck], ctx: AuthContext) -> bool
Run auth checks with AND logic.
All checks must pass for authorization to succeed.
All checks must pass for authorization to succeed. Checks can be
synchronous or asynchronous functions.
Auth checks can:
- Return True to allow access
@ -88,6 +89,7 @@ Auth checks can:
**Args:**
- `checks`: A single check function or list of check functions.
Each check can be sync (returns bool) or async (returns Awaitable[bool]).
- `ctx`: The auth context to pass to each check.
**Returns:**
@ -99,7 +101,7 @@ Auth checks can:
## Classes
### `AuthContext` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L47" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `AuthContext` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L48" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Context passed to auth check callables.
@ -115,7 +117,7 @@ access to the current authentication token and the component being accessed.
**Methods:**
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/authorization.py#L64" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self) -> Tool | None

View file

@ -12,9 +12,38 @@ This provider implements Azure/Microsoft Entra ID OAuth authentication
using the OAuth Proxy pattern for non-DCR OAuth flows.
## Functions
### `EntraOBOToken` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L658" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
EntraOBOToken(scopes: list[str]) -> str
```
Exchange the user's Entra token for a downstream API token via OBO.
This dependency performs a Microsoft Entra On-Behalf-Of (OBO) token exchange,
allowing your MCP server to call downstream APIs (like Microsoft Graph) on
behalf of the authenticated user.
**Args:**
- `scopes`: The scopes to request for the downstream API. For Microsoft Graph,
use scopes like ["https\://graph.microsoft.com/Mail.Read"] or
["https\://graph.microsoft.com/.default"].
**Returns:**
- A dependency that resolves to the downstream API access token string
**Raises:**
- `ImportError`: If fastmcp[azure] is not installed
- `RuntimeError`: If no access token is available, provider is not Azure,
or OBO exchange fails
## Classes
### `AzureProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L30" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `AzureProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L31" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Azure (Microsoft Entra) OAuth provider for FastMCP.
@ -49,7 +78,7 @@ Setup:
**Methods:**
#### `authorize` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L230" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `authorize` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L235" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
@ -69,7 +98,29 @@ scopes to determine the resource/audience instead of a separate parameter.
- Authorization URL to redirect the user to Azure AD
### `AzureJWTVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L457" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `create_obo_credential` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L461" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_obo_credential(self, user_assertion: str) -> OnBehalfOfCredential
```
Create an OnBehalfOfCredential for OBO token exchange.
Uses the AzureProvider's configuration (client_id, client_secret,
tenant_id, authority) to create a credential that can exchange the
user's token for downstream API tokens.
**Args:**
- `user_assertion`: The user's access token to exchange via OBO.
**Returns:**
- A configured OnBehalfOfCredential ready for get_token() calls.
**Raises:**
- `ImportError`: If azure-identity is not installed (requires fastmcp[azure]).
### `AzureJWTVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L489" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
JWT verifier pre-configured for Azure AD / Microsoft Entra ID.
@ -106,7 +157,7 @@ Example::
**Methods:**
#### `scopes_supported` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L537" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `scopes_supported` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L569" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
scopes_supported(self) -> list[str]

View file

@ -77,6 +77,9 @@ async def my_tool(x: int, ctx: Context) -> str:
await ctx.set_state("key", "value")
value = await ctx.get_state("key")
# Store non-serializable values for the current request only
await ctx.set_state("client", http_client, serializable=False)
return str(x)
```
@ -96,7 +99,7 @@ The context is optional - tools that don't need it can omit the parameter.
**Methods:**
#### `is_background_task` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L199" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `is_background_task` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L204" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
is_background_task(self) -> bool
@ -109,7 +112,7 @@ task-aware implementations that can pause the task and wait for
client input.
#### `task_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L218" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `task_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L223" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
task_id(self) -> str | None
@ -120,7 +123,7 @@ Get the background task ID if running in a background task.
Returns None if not running in a background task context.
#### `fastmcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L226" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `fastmcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L231" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
fastmcp(self) -> FastMCP
@ -129,7 +132,7 @@ fastmcp(self) -> FastMCP
Get the FastMCP instance.
#### `request_context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L285" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `request_context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L290" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
request_context(self) -> RequestContext[ServerSession, Any, Request] | None
@ -158,7 +161,7 @@ async def on_request(self, context, call_next):
```
#### `lifespan_context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L314" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `lifespan_context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L319" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
lifespan_context(self) -> dict[str, Any]
@ -170,6 +173,10 @@ Returns the context dict yielded by the server's lifespan function.
Returns an empty dict if no lifespan was configured or if the MCP
session is not yet established.
In background tasks (Docket workers), where request_context is not
available, falls back to reading from the FastMCP server's lifespan
result directly.
Example:
```python
@server.tool
@ -181,7 +188,7 @@ def my_tool(ctx: Context) -> str:
```
#### `report_progress` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L336" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `report_progress` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L350" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None
@ -189,12 +196,16 @@ report_progress(self, progress: float, total: float | None = None, message: str
Report progress for the current operation.
Works in both foreground (MCP progress notifications) and background
(Docket task execution) contexts.
**Args:**
- `progress`: Current progress value e.g. 24
- `total`: Optional total value e.g. 100
- `message`: Optional status message describing current progress
#### `list_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L390" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L444" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resources(self) -> list[SDKResource]
@ -206,7 +217,7 @@ List all available resources from the server.
- List of Resource objects available on the server
#### `list_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L406" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L460" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_prompts(self) -> list[SDKPrompt]
@ -218,7 +229,7 @@ List all available prompts from the server.
- List of Prompt objects available on the server
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L422" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L476" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult
@ -234,7 +245,7 @@ Get a prompt by name with optional arguments.
- The prompt result
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L441" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L495" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource(self, uri: str | AnyUrl) -> ResourceResult
@ -249,7 +260,7 @@ Read a resource by URI.
- ResourceResult with contents
#### `log` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L457" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `log` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L511" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
log(self, message: str, level: LoggingLevel | None = None, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@ -267,7 +278,7 @@ Messages sent to Clients are also logged to the `fastmcp.server.context.to_clien
- `extra`: Optional mapping for additional arguments
#### `transport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L486" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `transport` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L540" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
transport(self) -> TransportType | None
@ -279,7 +290,7 @@ Returns the transport type used to run this server: "stdio", "sse",
or "streamable-http". Returns None if called outside of a server context.
#### `client_supports_extension` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L494" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `client_supports_extension` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L548" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
client_supports_extension(self, extension_id: str) -> bool
@ -304,7 +315,7 @@ Example::
return "text-only client"
#### `client_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L522" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `client_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L576" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
client_id(self) -> str | None
@ -313,7 +324,7 @@ client_id(self) -> str | None
Get the client ID if available.
#### `request_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L531" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `request_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L585" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
request_id(self) -> str
@ -324,7 +335,7 @@ Get the unique ID for this request.
Raises RuntimeError if MCP request context is not available.
#### `session_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L544" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `session_id` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L598" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
session_id(self) -> str
@ -341,7 +352,7 @@ the same client session.
- for other transports.
#### `session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L601" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L655" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
session(self) -> ServerSession
@ -355,7 +366,7 @@ In background task mode: Returns the session stored at Context creation.
Raises RuntimeError if no session is available.
#### `debug` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L627" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `debug` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L681" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
debug(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@ -366,7 +377,7 @@ Send a `DEBUG`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
#### `info` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L643" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `info` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L697" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
info(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@ -377,7 +388,7 @@ Send a `INFO`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
#### `warning` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L659" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `warning` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L713" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
warning(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@ -388,7 +399,7 @@ Send a `WARNING`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
#### `error` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L675" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `error` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L729" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
error(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@ -399,7 +410,7 @@ Send a `ERROR`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
#### `list_roots` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L691" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_roots` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L745" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_roots(self) -> list[Root]
@ -408,7 +419,7 @@ list_roots(self) -> list[Root]
List the roots available to the server, as indicated by the client.
#### `send_notification` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L696" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `send_notification` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L750" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
send_notification(self, notification: mcp.types.ServerNotificationType) -> None
@ -420,7 +431,7 @@ Send a notification to the client immediately.
- `notification`: An MCP notification instance (e.g., ToolListChangedNotification())
#### `close_sse_stream` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L706" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `close_sse_stream` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L760" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
close_sse_stream(self) -> None
@ -438,7 +449,7 @@ Instead of holding a connection open for minutes, you can periodically close
and let the client reconnect.
#### `sample_step` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L745" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `sample_step` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L799" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
sample_step(self, messages: str | Sequence[str | SamplingMessage]) -> SampleStep
@ -465,6 +476,12 @@ in the step for manual execution.
- `mask_error_details`: If True, mask detailed error messages from tool
execution. When None (default), uses the global settings value.
Tools can raise ToolError to bypass masking.
- `tool_concurrency`: Controls parallel execution of tools\:
- None (default)\: Sequential execution (one at a time)
- 0\: Unlimited parallel execution
- N > 0\: Execute at most N tools concurrently
If any tool has sequential=True, all tools execute sequentially
regardless of this setting.
**Returns:**
- SampleStep containing:
@ -475,7 +492,7 @@ Tools can raise ToolError to bypass masking.
- - .text: The text content (if any)
#### `sample` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L816" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `sample` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L878" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT]
@ -484,7 +501,7 @@ sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[
Overload: With result_type, returns SamplingResult[ResultT].
#### `sample` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L831" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `sample` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L894" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[str]
@ -493,7 +510,7 @@ sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[
Overload: Without result_type, returns SamplingResult[str].
#### `sample` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L845" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `sample` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L909" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT] | SamplingResult[str]
@ -527,6 +544,12 @@ response is validated against this type.
- `mask_error_details`: If True, mask detailed error messages from tool
execution. When None (default), uses the global settings value.
Tools can raise ToolError to bypass masking.
- `tool_concurrency`: Controls parallel execution of tools\:
- None (default)\: Sequential execution (one at a time)
- 0\: Unlimited parallel execution
- N > 0\: Execute at most N tools concurrently
If any tool has sequential=True, all tools execute sequentially
regardless of this setting.
**Returns:**
- SamplingResult[T] containing:
@ -535,43 +558,43 @@ Tools can raise ToolError to bypass masking.
- - .history: All messages exchanged during sampling
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L912" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L984" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: None) -> AcceptedElicitation[dict[str, Any]] | DeclinedElicitation | CancelledElicitation
```
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L924" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L996" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: type[T]) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation
```
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L934" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1006" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: list[str]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation
```
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L944" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1016" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: dict[str, dict[str, str]]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation
```
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L954" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1026" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: list[list[str]]) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
```
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L966" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1038" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: list[dict[str, dict[str, str]]]) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
```
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L978" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `elicit` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1050" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: type[T] | list[str] | dict[str, dict[str, str]] | list[list[str]] | list[dict[str, dict[str, str]]] | None = None) -> AcceptedElicitation[T] | AcceptedElicitation[dict[str, Any]] | AcceptedElicitation[str] | AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
@ -600,40 +623,53 @@ type or dataclass or BaseModel. If it is a primitive type, an
object schema with a single "value" field will be generated.
#### `set_state` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1092" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `set_state` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1164" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_state(self, key: str, value: Any) -> None
```
Set a value in the session-scoped state store.
Set a value in the state store.
By default, values are stored in the session-scoped state store and
persist across requests within the same MCP session. Values must be
JSON-serializable (dicts, lists, strings, numbers, etc.).
For non-serializable values (e.g., HTTP clients, database connections),
pass ``serializable=False``. These values are stored in a request-scoped
dict and only live for the current MCP request (tool call, resource
read, or prompt render). They will not be available in subsequent
requests.
Values persist across requests within the same MCP session.
The key is automatically prefixed with the session identifier.
State expires after 1 day to prevent unbounded memory growth.
#### `get_state` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_state` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1206" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_state(self, key: str) -> Any
```
Get a value from the session-scoped state store.
Get a value from the state store.
Checks request-scoped state first (set with ``serializable=False``),
then falls back to the session-scoped state store.
Returns None if the key is not found.
#### `delete_state` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1115" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `delete_state` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1220" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
delete_state(self, key: str) -> None
```
Delete a value from the session-scoped state store.
Delete a value from the state store.
Removes from both request-scoped and session-scoped stores.
#### `enable_components` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1132" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `enable_components` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1241" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
enable_components(self) -> None
@ -657,7 +693,7 @@ ResourceListChangedNotification, and PromptListChangedNotification.
- `match_all`: If True, matches all components regardless of other criteria.
#### `disable_components` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1170" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `disable_components` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1279" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
disable_components(self) -> None
@ -681,7 +717,7 @@ ResourceListChangedNotification, and PromptListChangedNotification.
- `match_all`: If True, matches all components regardless of other criteria.
#### `reset_visibility` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1208" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `reset_visibility` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/context.py#L1317" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
reset_visibility(self) -> None

View file

@ -15,7 +15,7 @@ CurrentWorker) and background task execution require fastmcp[tasks].
## Functions
### `get_task_context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L90" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `get_task_context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L95" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_task_context() -> TaskContextInfo | None
@ -31,7 +31,7 @@ Returns None if not running in a task context (e.g., foreground execution).
- TaskContextInfo with task_id and session_id, or None if not in a task.
### `register_task_session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L128" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `register_task_session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L133" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
register_task_session(session_id: str, session: ServerSession) -> None
@ -49,7 +49,7 @@ client disconnects.
- `session`: The ServerSession instance
### `get_task_session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L142" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `get_task_session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L147" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_task_session(session_id: str) -> ServerSession | None
@ -65,7 +65,7 @@ Get a registered session by ID if still alive.
- The ServerSession if found and alive, None otherwise
### `is_docket_available` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L175" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `is_docket_available` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L183" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
is_docket_available() -> bool
@ -75,7 +75,7 @@ is_docket_available() -> bool
Check if pydocket is installed.
### `require_docket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L188" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `require_docket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L196" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
require_docket(feature: str) -> None
@ -89,7 +89,7 @@ Raise ImportError with install instructions if docket not available.
"CurrentDocket()"). Will be included in the error message.
### `transform_context_annotations` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L230" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `transform_context_annotations` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L238" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
transform_context_annotations(fn: Callable[..., Any]) -> Callable[..., Any]
@ -115,7 +115,7 @@ allows them to have defaults in any order.
- Function with modified signature (same function object, updated __signature__)
### `get_context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L381" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `get_context` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L389" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_context() -> Context
@ -125,7 +125,7 @@ get_context() -> Context
Get the current FastMCP Context instance directly.
### `get_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L391" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `get_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L399" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_server() -> FastMCP
@ -141,7 +141,7 @@ Get the current FastMCP server instance directly.
- `RuntimeError`: If no server in context
### `get_http_request` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L409" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `get_http_request` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L417" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_http_request() -> Request
@ -153,7 +153,7 @@ Get the current HTTP request.
Tries MCP SDK's request_ctx first, then falls back to FastMCP's HTTP context.
### `get_http_headers` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L429" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `get_http_headers` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L437" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_http_headers(include_all: bool = False) -> dict[str, str]
@ -169,7 +169,7 @@ By default, strips problematic headers like `content-length` that cause issues
if forwarded to downstream clients. If `include_all` is True, all headers are returned.
### `get_access_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L475" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `get_access_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L483" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_access_token() -> AccessToken | None
@ -181,13 +181,14 @@ Get the FastMCP access token from the current context.
This function first tries to get the token from the current HTTP request's scope,
which is more reliable for long-lived connections where the SDK's auth_context_var
may become stale after token refresh. Falls back to the SDK's context var if no
request is available.
request is available. In background tasks (Docket workers), falls back to the
token snapshot stored in Redis at task submission time.
**Returns:**
- The access token if an authenticated user is available, None otherwise.
### `without_injected_parameters` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L533" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `without_injected_parameters` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L555" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
without_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any]
@ -212,7 +213,7 @@ Handles:
- Async wrapper function without injected parameters
### `resolve_dependencies` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L674" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `resolve_dependencies` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L696" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
resolve_dependencies(fn: Callable[..., Any], arguments: dict[str, Any]) -> AsyncGenerator[dict[str, Any], None]
@ -238,7 +239,7 @@ time, so all injection goes through the unified DI system.
which will be filtered out)
### `CurrentContext` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L770" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `CurrentContext` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L837" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
CurrentContext() -> Context
@ -257,7 +258,7 @@ current MCP operation (tool/resource/prompt call).
- `RuntimeError`: If no active context found (during resolution)
### `CurrentDocket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L813" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `CurrentDocket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L880" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
CurrentDocket() -> Docket
@ -277,7 +278,7 @@ automatically creates for background task scheduling.
- `ImportError`: If fastmcp[tasks] not installed
### `CurrentWorker` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L858" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `CurrentWorker` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L925" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
CurrentWorker() -> Worker
@ -297,7 +298,7 @@ automatically creates for background task processing.
- `ImportError`: If fastmcp[tasks] not installed
### `CurrentFastMCP` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L900" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `CurrentFastMCP` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L967" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
CurrentFastMCP() -> FastMCP
@ -315,7 +316,7 @@ This dependency provides access to the active FastMCP server.
- `RuntimeError`: If no server in context (during resolution)
### `CurrentRequest` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L935" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `CurrentRequest` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1002" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
CurrentRequest() -> Request
@ -335,7 +336,7 @@ current HTTP request. Only available when running over HTTP transports
- `RuntimeError`: If no HTTP request in context (e.g., STDIO transport)
### `CurrentHeaders` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L971" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `CurrentHeaders` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1038" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
CurrentHeaders() -> dict[str, str]
@ -352,7 +353,7 @@ safe to use in code that might run over any transport.
- A dependency that resolves to a dictionary of header name -> value
### `CurrentAccessToken` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1010" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `CurrentAccessToken` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1228" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
CurrentAccessToken() -> AccessToken
@ -371,9 +372,32 @@ authenticated request. Raises an error if no authentication is present.
- `RuntimeError`: If no authenticated user (use get_access_token() for optional)
### `TokenClaim` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1280" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
TokenClaim(name: str) -> str
```
Get a specific claim from the access token.
This dependency extracts a single claim value from the current access token.
It's useful for getting user identifiers, roles, or other token claims
without needing the full token object.
**Args:**
- `name`: The name of the claim to extract (e.g., "oid", "sub", "email")
**Returns:**
- A dependency that resolves to the claim value as a string
**Raises:**
- `RuntimeError`: If no access token is available or claim is missing
## Classes
### `TaskContextInfo` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L76" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `TaskContextInfo` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L81" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Information about the current background task context.
@ -382,7 +406,7 @@ Returned by ``get_task_context()`` when running inside a Docket worker.
Contains identifiers needed to communicate with the MCP session.
### `ProgressLike` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1039" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ProgressLike` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1065" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Protocol for progress tracking interface.
@ -393,7 +417,7 @@ and Docket's Progress (worker context).
**Methods:**
#### `current` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1047" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `current` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1073" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
current(self) -> int | None
@ -402,7 +426,7 @@ current(self) -> int | None
Current progress value.
#### `total` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1052" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `total` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1078" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
total(self) -> int
@ -411,7 +435,7 @@ total(self) -> int
Total/target progress value.
#### `message` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1057" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `message` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1083" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
message(self) -> str | None
@ -420,7 +444,7 @@ message(self) -> str | None
Current progress message.
#### `set_total` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1061" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `set_total` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1087" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_total(self, total: int) -> None
@ -429,7 +453,7 @@ set_total(self, total: int) -> None
Set the total/target value for progress tracking.
#### `increment` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1065" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `increment` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1091" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
increment(self, amount: int = 1) -> None
@ -438,7 +462,7 @@ increment(self, amount: int = 1) -> None
Atomically increment the current progress value.
#### `set_message` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1069" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `set_message` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1095" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_message(self, message: str | None) -> None
@ -447,7 +471,7 @@ set_message(self, message: str | None) -> None
Update the progress status message.
### `InMemoryProgress` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1074" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `InMemoryProgress` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1100" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
In-memory progress tracker for immediate tool execution.
@ -459,25 +483,25 @@ progress doesn't need to be observable across processes.
**Methods:**
#### `current` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1094" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `current` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1120" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
current(self) -> int | None
```
#### `total` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1098" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `total` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1124" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
total(self) -> int
```
#### `message` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1102" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `message` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1128" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
message(self) -> str | None
```
#### `set_total` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1105" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `set_total` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1131" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_total(self, total: int) -> None
@ -486,7 +510,7 @@ set_total(self, total: int) -> None
Set the total/target value for progress tracking.
#### `increment` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1111" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `increment` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1137" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
increment(self, amount: int = 1) -> None
@ -495,7 +519,7 @@ increment(self, amount: int = 1) -> None
Atomically increment the current progress value.
#### `set_message` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1120" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `set_message` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1146" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_message(self, message: str | None) -> None
@ -504,7 +528,7 @@ set_message(self, message: str | None) -> None
Update the progress status message.
### `Progress` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1125" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `Progress` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/dependencies.py#L1151" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
FastMCP Progress dependency that works in both server and worker contexts.

View file

@ -151,7 +151,7 @@ Notes:
**Methods:**
#### `on_list_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L287" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `on_list_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L285" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
on_list_tools(self, context: MiddlewareContext[mcp.types.ListToolsRequest], call_next: CallNext[mcp.types.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]
@ -161,7 +161,7 @@ List tools from the cache, if caching is enabled, and the result is in the cache
otherwise call the next middleware and store the result in the cache if caching is enabled.
#### `on_list_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L326" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `on_list_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L324" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
on_list_resources(self, context: MiddlewareContext[mcp.types.ListResourcesRequest], call_next: CallNext[mcp.types.ListResourcesRequest, Sequence[Resource]]) -> Sequence[Resource]
@ -171,7 +171,7 @@ List resources from the cache, if caching is enabled, and the result is in the c
otherwise call the next middleware and store the result in the cache if caching is enabled.
#### `on_list_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L365" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `on_list_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L363" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
on_list_prompts(self, context: MiddlewareContext[mcp.types.ListPromptsRequest], call_next: CallNext[mcp.types.ListPromptsRequest, Sequence[Prompt]]) -> Sequence[Prompt]
@ -181,7 +181,7 @@ List prompts from the cache, if caching is enabled, and the result is in the cac
otherwise call the next middleware and store the result in the cache if caching is enabled.
#### `on_call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L402" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `on_call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L400" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
on_call_tool(self, context: MiddlewareContext[mcp.types.CallToolRequestParams], call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult]) -> ToolResult
@ -191,7 +191,7 @@ Call a tool from the cache, if caching is enabled, and the result is in the cach
otherwise call the next middleware and store the result in the cache if caching is enabled.
#### `on_read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L435" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `on_read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L433" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
on_read_resource(self, context: MiddlewareContext[mcp.types.ReadResourceRequestParams], call_next: CallNext[mcp.types.ReadResourceRequestParams, ResourceResult]) -> ResourceResult
@ -201,7 +201,7 @@ Read a resource from the cache, if caching is enabled, and the result is in the
otherwise call the next middleware and store the result in the cache if caching is enabled.
#### `on_get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L463" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `on_get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L461" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
on_get_prompt(self, context: MiddlewareContext[mcp.types.GetPromptRequestParams], call_next: CallNext[mcp.types.GetPromptRequestParams, PromptResult]) -> PromptResult
@ -211,7 +211,7 @@ Get a prompt from the cache, if caching is enabled, and the result is in the cac
otherwise call the next middleware and store the result in the cache if caching is enabled.
#### `statistics` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L501" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `statistics` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/caching.py#L499" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
statistics(self) -> ResponseCachingStatistics

View file

@ -0,0 +1,35 @@
---
title: dereference
sidebarTitle: dereference
---
# `fastmcp.server.middleware.dereference`
Middleware that dereferences $ref in JSON schemas before sending to clients.
## Classes
### `DereferenceRefsMiddleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/dereference.py#L15" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Dereferences $ref in component schemas before sending to clients.
Some MCP clients (e.g., VS Code Copilot) don't handle JSON Schema $ref
properly. This middleware inlines all $ref definitions so schemas are
self-contained. Enabled by default via ``FastMCP(dereference_schemas=True)``.
**Methods:**
#### `on_list_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/dereference.py#L24" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
on_list_tools(self, context: MiddlewareContext[mt.ListToolsRequest], call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]
```
#### `on_list_resource_templates` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/dereference.py#L33" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
on_list_resource_templates(self, context: MiddlewareContext[mt.ListResourceTemplatesRequest], call_next: CallNext[mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]]) -> Sequence[ResourceTemplate]
```

View file

@ -104,7 +104,7 @@ Run the server using HTTP transport.
- `stateless`: Alias for stateless_http for CLI consistency
#### `http_app` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/mixins/transport.py#L281" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `http_app` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/mixins/transport.py#L279" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
http_app(self: FastMCP, path: str | None = None, middleware: list[ASGIMiddleware] | None = None, json_response: bool | None = None, stateless_http: bool | None = None, transport: Literal['http', 'streamable-http', 'sse'] = 'http', event_store: EventStore | None = None, retry_interval: int | None = None) -> StarletteWithLifespan

View file

@ -14,7 +14,7 @@ registration functionality to LocalProvider.
## Classes
### `ToolDecoratorMixin` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L31" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ToolDecoratorMixin` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L32" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Mixin class providing tool decorator functionality for LocalProvider.
@ -26,7 +26,7 @@ This mixin contains all methods related to:
**Methods:**
#### `add_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L39" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L40" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_tool(self: LocalProvider, tool: Tool | Callable[..., Any]) -> Tool
@ -37,19 +37,19 @@ Add a tool to this provider's storage.
Accepts either a Tool object or a decorated function with __fastmcp__ metadata.
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L78" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L91" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self: LocalProvider, name_or_fn: AnyFunction) -> FunctionTool
```
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L100" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L113" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self: LocalProvider, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionTool]
```
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L125" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/local_provider/decorators/tools.py#L138" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self: LocalProvider, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]

View file

@ -27,7 +27,7 @@ run(self, arguments: dict[str, Any]) -> ToolResult
Execute the HTTP request using RequestDirector.
### `OpenAPIResource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L222" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `OpenAPIResource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L233" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Resource implementation for OpenAPI endpoints.
@ -35,7 +35,7 @@ Resource implementation for OpenAPI endpoints.
**Methods:**
#### `read` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L252" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L263" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read(self) -> ResourceResult
@ -44,7 +44,7 @@ read(self) -> ResourceResult
Fetch the resource data by making an HTTP request.
### `OpenAPIResourceTemplate` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L336" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `OpenAPIResourceTemplate` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L347" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Resource template implementation for OpenAPI endpoints.
@ -52,7 +52,7 @@ Resource template implementation for OpenAPI endpoints.
**Methods:**
#### `create_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L368" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `create_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/components.py#L379" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_resource(self, uri: str, params: dict[str, Any], context: Context | None = None) -> Resource

View file

@ -10,7 +10,7 @@ OpenAPIProvider for creating MCP components from OpenAPI specifications.
## Classes
### `OpenAPIProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/provider.py#L52" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `OpenAPIProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/provider.py#L51" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Provider that creates MCP components from an OpenAPI specification.
@ -21,7 +21,7 @@ spec. Each component makes HTTP calls to the described API endpoints.
**Methods:**
#### `lifespan` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/provider.py#L176" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `lifespan` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/provider.py#L181" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
lifespan(self) -> AsyncIterator[None]
@ -30,7 +30,7 @@ lifespan(self) -> AsyncIterator[None]
Manage the lifecycle of the auto-created httpx client.
#### `get_tasks` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/provider.py#L430" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tasks` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/openapi/provider.py#L431" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tasks(self) -> Sequence[FastMCPComponent]

View file

@ -15,7 +15,7 @@ classes that forward execution to remote servers.
## Functions
### `default_proxy_roots_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L713" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `default_proxy_roots_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L720" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
default_proxy_roots_handler(context: RequestContext[ClientSession, LifespanContextT]) -> RootsList
@ -25,7 +25,7 @@ default_proxy_roots_handler(context: RequestContext[ClientSession, LifespanConte
Forward list roots request from remote server to proxy's connected clients.
### `default_proxy_sampling_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L721" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `default_proxy_sampling_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L728" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
default_proxy_sampling_handler(messages: list[mcp.types.SamplingMessage], params: mcp.types.CreateMessageRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> mcp.types.CreateMessageResult
@ -35,7 +35,7 @@ default_proxy_sampling_handler(messages: list[mcp.types.SamplingMessage], params
Forward sampling request from remote server to proxy's connected clients.
### `default_proxy_elicitation_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L744" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `default_proxy_elicitation_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L751" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
default_proxy_elicitation_handler(message: str, response_type: type, params: mcp.types.ElicitRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> ElicitResult
@ -45,7 +45,7 @@ default_proxy_elicitation_handler(message: str, response_type: type, params: mcp
Forward elicitation request from remote server to proxy's connected clients.
### `default_proxy_log_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L766" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `default_proxy_log_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L773" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
default_proxy_log_handler(message: LogMessage) -> None
@ -55,7 +55,7 @@ default_proxy_log_handler(message: LogMessage) -> None
Forward log notification from remote server to proxy's connected clients.
### `default_proxy_progress_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L774" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `default_proxy_progress_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L781" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
default_proxy_progress_handler(progress: float, total: float | None, message: str | None) -> None
@ -67,7 +67,7 @@ Forward progress notification from remote server to proxy's connected clients.
## Classes
### `ProxyTool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L66" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ProxyTool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A Tool that represents and executes a tool on a remote server.
@ -75,7 +75,7 @@ A Tool that represents and executes a tool on a remote server.
**Methods:**
#### `model_copy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L83" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `model_copy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L84" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
model_copy(self, **kwargs: Any) -> ProxyTool
@ -84,7 +84,7 @@ model_copy(self, **kwargs: Any) -> ProxyTool
Override to preserve _backend_name when name changes.
#### `from_mcp_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L93" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_mcp_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L94" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_mcp_tool(cls, client_factory: ClientFactoryT, mcp_tool: mcp.types.Tool) -> ProxyTool
@ -93,7 +93,7 @@ from_mcp_tool(cls, client_factory: ClientFactoryT, mcp_tool: mcp.types.Tool) ->
Factory method to create a ProxyTool from a raw MCP tool schema.
#### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L110" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L111" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run(self, arguments: dict[str, Any], context: Context | None = None) -> ToolResult
@ -102,13 +102,13 @@ run(self, arguments: dict[str, Any], context: Context | None = None) -> ToolResu
Executes the tool by making a call through the client.
#### `get_span_attributes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L156" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_span_attributes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L163" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_span_attributes(self) -> dict[str, Any]
```
### `ProxyResource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L163" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ProxyResource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L170" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A Resource that represents and reads a resource from a remote server.
@ -116,7 +116,7 @@ A Resource that represents and reads a resource from a remote server.
**Methods:**
#### `model_copy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L188" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `model_copy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L195" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
model_copy(self, **kwargs: Any) -> ProxyResource
@ -125,7 +125,7 @@ model_copy(self, **kwargs: Any) -> ProxyResource
Override to preserve _backend_uri when uri changes.
#### `from_mcp_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L198" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_mcp_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L205" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_mcp_resource(cls, client_factory: ClientFactoryT, mcp_resource: mcp.types.Resource) -> ProxyResource
@ -134,7 +134,7 @@ from_mcp_resource(cls, client_factory: ClientFactoryT, mcp_resource: mcp.types.R
Factory method to create a ProxyResource from a raw MCP resource schema.
#### `read` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L218" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L225" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read(self) -> ResourceResult
@ -143,13 +143,13 @@ read(self) -> ResourceResult
Read the resource content from the remote server.
#### `get_span_attributes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L263" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_span_attributes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L270" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_span_attributes(self) -> dict[str, Any]
```
### `ProxyTemplate` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L270" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ProxyTemplate` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L277" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A ResourceTemplate that represents and creates resources from a remote server template.
@ -157,7 +157,7 @@ A ResourceTemplate that represents and creates resources from a remote server te
**Methods:**
#### `model_copy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L287" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `model_copy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L294" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
model_copy(self, **kwargs: Any) -> ProxyTemplate
@ -166,7 +166,7 @@ model_copy(self, **kwargs: Any) -> ProxyTemplate
Override to preserve _backend_uri_template when uri_template changes.
#### `from_mcp_template` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L297" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_mcp_template` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L304" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_mcp_template(cls, client_factory: ClientFactoryT, mcp_template: mcp.types.ResourceTemplate) -> ProxyTemplate
@ -175,7 +175,7 @@ from_mcp_template(cls, client_factory: ClientFactoryT, mcp_template: mcp.types.R
Factory method to create a ProxyTemplate from a raw MCP template schema.
#### `create_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L316" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `create_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L323" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_resource(self, uri: str, params: dict[str, Any], context: Context | None = None) -> ProxyResource
@ -184,13 +184,13 @@ create_resource(self, uri: str, params: dict[str, Any], context: Context | None
Create a resource from the template by calling the remote server.
#### `get_span_attributes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L378" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_span_attributes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L385" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_span_attributes(self) -> dict[str, Any]
```
### `ProxyPrompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L385" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ProxyPrompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L392" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A Prompt that represents and renders a prompt from a remote server.
@ -198,7 +198,7 @@ A Prompt that represents and renders a prompt from a remote server.
**Methods:**
#### `model_copy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L402" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `model_copy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L409" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
model_copy(self, **kwargs: Any) -> ProxyPrompt
@ -207,7 +207,7 @@ model_copy(self, **kwargs: Any) -> ProxyPrompt
Override to preserve _backend_name when name changes.
#### `from_mcp_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L412" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_mcp_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L419" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_mcp_prompt(cls, client_factory: ClientFactoryT, mcp_prompt: mcp.types.Prompt) -> ProxyPrompt
@ -216,7 +216,7 @@ from_mcp_prompt(cls, client_factory: ClientFactoryT, mcp_prompt: mcp.types.Promp
Factory method to create a ProxyPrompt from a raw MCP prompt schema.
#### `render` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L436" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `render` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L443" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
render(self, arguments: dict[str, Any]) -> PromptResult
@ -225,13 +225,13 @@ render(self, arguments: dict[str, Any]) -> PromptResult
Render the prompt by making a call through the client.
#### `get_span_attributes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L458" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_span_attributes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L465" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_span_attributes(self) -> dict[str, Any]
```
### `ProxyProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L470" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ProxyProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L477" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Provider that proxies to a remote MCP server via a client factory.
@ -245,7 +245,7 @@ because tasks cannot be executed through a proxy.
**Methods:**
#### `get_tasks` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L595" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tasks` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L602" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tasks(self) -> Sequence[FastMCPComponent]
@ -258,7 +258,7 @@ server lifespan initialization, which would open the client before any
context is set. All Proxy* components have task_config.mode="forbidden".
### `FastMCPProxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L666" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `FastMCPProxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L673" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A FastMCP server that acts as a proxy to a remote MCP-compliant server.
@ -267,7 +267,7 @@ This is a convenience wrapper that creates a FastMCP server with a
ProxyProvider. For more control, use FastMCP with add_provider(ProxyProvider(...)).
### `ProxyClient` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L784" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ProxyClient` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L829" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A proxy client that forwards advanced interactions between a remote MCP server and the proxy's connected clients.
@ -275,7 +275,7 @@ A proxy client that forwards advanced interactions between a remote MCP server a
Supports forwarding roots, sampling, elicitation, logging, and progress.
### `StatefulProxyClient` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L821" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `StatefulProxyClient` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L862" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A proxy client that provides a stateful client factory for the proxy server.
@ -286,10 +286,17 @@ And it will be disconnected when the session is exited.
This is useful to proxy a stateful mcp server such as the Playwright MCP server.
Note that it is essential to ensure that the proxy server itself is also stateful.
Because session reuse means the receive-loop task inherits a stale
``request_ctx`` ContextVar snapshot, the default proxy handlers are
replaced with versions that restore the ContextVar before forwarding.
``ProxyTool.run`` stashes the current ``RequestContext`` in
``_proxy_rc_ref`` before each backend call, and the handlers consult
it to detect (and correct) staleness.
**Methods:**
#### `clear` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L841" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `clear` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L912" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
clear(self)
@ -298,7 +305,7 @@ clear(self)
Clear all cached clients and force disconnect them.
#### `new_stateful` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L847" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `new_stateful` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/providers/proxy.py#L918" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
new_stateful(self) -> Client[ClientTransportT]

View file

@ -10,7 +10,7 @@ Sampling types and helper functions for FastMCP servers.
## Functions
### `determine_handler_mode` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L128" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `determine_handler_mode` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L132" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
determine_handler_mode(context: Context, needs_tools: bool) -> bool
@ -30,7 +30,7 @@ Determine whether to use fallback handler or client for sampling.
- `ValueError`: If client lacks required capability and no fallback configured.
### `call_sampling_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L187" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `call_sampling_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L191" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
call_sampling_handler(context: Context, messages: list[SamplingMessage]) -> CreateMessageResult | CreateMessageResultWithTools
@ -44,10 +44,10 @@ sampling_handler is set via determine_handler_mode(). The checks below are
safeguards against internal misuse.
### `execute_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L238" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `execute_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L242" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
execute_tools(tool_calls: list[ToolUseContent], tool_map: dict[str, SamplingTool], mask_error_details: bool = False) -> list[ToolResultContent]
execute_tools(tool_calls: list[ToolUseContent], tool_map: dict[str, SamplingTool], mask_error_details: bool = False, tool_concurrency: int | None = None) -> list[ToolResultContent]
```
@ -60,12 +60,18 @@ Execute tool calls and return results.
When masked, only generic error messages are returned to the LLM.
Tools can explicitly raise ToolError to bypass masking when they want
to provide specific error messages to the LLM.
- `tool_concurrency`: Controls parallel execution of tools\:
- None (default)\: Sequential execution (one at a time)
- 0\: Unlimited parallel execution
- N > 0\: Execute at most N tools concurrently
If any tool has sequential=True, all tools execute sequentially
regardless of this setting.
**Returns:**
- List of tool result content blocks.
- List of tool result content blocks in the same order as tool_calls.
### `prepare_messages` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L317" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `prepare_messages` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L352" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
prepare_messages(messages: str | Sequence[str | SamplingMessage]) -> list[SamplingMessage]
@ -75,17 +81,28 @@ prepare_messages(messages: str | Sequence[str | SamplingMessage]) -> list[Sampli
Convert various message formats to a list of SamplingMessage objects.
### `prepare_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L336" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `prepare_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L371" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
prepare_tools(tools: Sequence[SamplingTool | Callable[..., Any]] | None) -> list[SamplingTool] | None
prepare_tools(tools: Sequence[SamplingTool | FunctionTool | TransformedTool | Callable[..., Any]] | None) -> list[SamplingTool] | None
```
Convert tools to SamplingTool objects.
Accepts SamplingTool instances, FunctionTool instances, TransformedTool instances,
or plain callable functions. FunctionTool and TransformedTool are converted using
from_callable_tool(), while plain functions use from_function().
### `extract_tool_calls` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L355" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
**Args:**
- `tools`: Sequence of tools to prepare. Can be SamplingTool, FunctionTool,
TransformedTool, or plain callable functions.
**Returns:**
- List of SamplingTool instances, or None if tools is None.
### `extract_tool_calls` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L407" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
extract_tool_calls(response: CreateMessageResult | CreateMessageResultWithTools) -> list[ToolUseContent]
@ -95,7 +112,7 @@ extract_tool_calls(response: CreateMessageResult | CreateMessageResultWithTools)
Extract tool calls from a response.
### `create_final_response_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L367" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `create_final_response_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L419" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_final_response_tool(result_type: type) -> SamplingTool
@ -108,7 +125,7 @@ This tool is used to capture structured responses from the LLM.
The tool's schema is derived from the result_type.
### `sample_step_impl` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L403" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `sample_step_impl` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L455" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
sample_step_impl(context: Context, messages: str | Sequence[str | SamplingMessage]) -> SampleStep
@ -121,7 +138,7 @@ Make a single LLM sampling call. This is a stateless function that makes
exactly one LLM call and optionally executes any requested tools.
### `sample_impl` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L515" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `sample_impl` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L572" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
sample_impl(context: Context, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT]
@ -137,7 +154,7 @@ provides a final text response.
## Classes
### `SamplingResult` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L50" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `SamplingResult` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Result of a sampling operation.
@ -148,7 +165,7 @@ Result of a sampling operation.
- `history`: All messages exchanged during sampling.
### `SampleStep` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L65" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `SampleStep` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L69" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Result of a single sampling call.
@ -158,7 +175,7 @@ Represents what the LLM returned in this step plus the message history.
**Methods:**
#### `is_tool_use` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L75" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `is_tool_use` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L79" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
is_tool_use(self) -> bool
@ -167,7 +184,7 @@ is_tool_use(self) -> bool
True if the LLM is requesting tool execution.
#### `text` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L82" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `text` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L86" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
text(self) -> str | None
@ -176,7 +193,7 @@ text(self) -> str | None
Extract text from the response, if available.
#### `tool_calls` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L95" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool_calls` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/run.py#L99" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool_calls(self) -> list[ToolUseContent]

View file

@ -10,7 +10,7 @@ SamplingTool for use during LLM sampling requests.
## Classes
### `SamplingTool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L16" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `SamplingTool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L20" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A tool that can be used during LLM sampling.
@ -37,7 +37,7 @@ Create a SamplingTool explicitly when you need custom name/description:
**Methods:**
#### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L46" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L51" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run(self, arguments: dict[str, Any] | None = None) -> Any
@ -52,7 +52,7 @@ Execute the tool with the given arguments.
- The result of executing the tool function.
#### `from_function` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L76" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_function` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L81" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_function(cls, fn: Callable[..., Any]) -> SamplingTool
@ -67,6 +67,10 @@ the tool's parameters. Type hints are used to determine parameter types.
- `fn`: The function to create a tool from.
- `name`: Optional name override. Defaults to the function's name.
- `description`: Optional description override. Defaults to the function's docstring.
- `sequential`: If True, this tool requires sequential execution and prevents
parallel execution of all tools in the batch. Set to True for tools
with shared state, file writes, or other operations that cannot run
concurrently. Defaults to False.
**Returns:**
- A SamplingTool wrapping the function.
@ -74,3 +78,24 @@ the tool's parameters. Type hints are used to determine parameter types.
**Raises:**
- `ValueError`: If the function is a lambda without a name override.
#### `from_callable_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/sampling/sampling_tool.py#L123" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_callable_tool(cls, tool: FunctionTool | TransformedTool) -> SamplingTool
```
Create a SamplingTool from a FunctionTool or TransformedTool.
Reuses existing server tools in sampling contexts. For TransformedTool,
the tool's .run() method is used to ensure proper argument transformation,
and the ToolResult is automatically unwrapped.
**Args:**
- `tool`: A FunctionTool or TransformedTool to convert.
- `name`: Optional name override. Defaults to tool.name.
- `description`: Optional description override. Defaults to tool.description.
**Raises:**
- `TypeError`: If the tool is not a FunctionTool or TransformedTool.

View file

@ -10,7 +10,7 @@ FastMCP - A more ergonomic interface for MCP servers.
## Functions
### `default_lifespan` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L175" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `default_lifespan` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L168" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]
@ -26,7 +26,7 @@ Default lifespan context manager that does nothing.
- An empty dictionary as the lifespan result.
### `create_proxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2154" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `create_proxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2079" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_proxy(target: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
@ -54,65 +54,74 @@ use `FastMCPProxy` or `ProxyProvider` directly from `fastmcp.server.providers.pr
## Classes
### `StateValue` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L211" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `StateValue` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L204" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Wrapper for stored context state values.
### `FastMCP` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L217" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `FastMCP` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L210" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
**Methods:**
#### `settings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L456" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
settings(self) -> Settings
```
#### `name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L467" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L345" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
name(self) -> str
```
#### `instructions` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L471" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `instructions` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L349" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
instructions(self) -> str | None
```
#### `instructions` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L475" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `instructions` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L353" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
instructions(self, value: str | None) -> None
```
#### `version` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L479" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `version` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L357" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
version(self) -> str | None
```
#### `website_url` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L483" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `website_url` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L361" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
website_url(self) -> str | None
```
#### `icons` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L487" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `icons` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L365" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
icons(self) -> list[mcp.types.Icon]
```
#### `add_middleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L504" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `local_provider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L372" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
local_provider(self) -> LocalProvider
```
The server's local provider, which stores directly-registered components.
Use this to remove components:
mcp.local_provider.remove_tool("my_tool")
mcp.local_provider.remove_resource("data://info")
mcp.local_provider.remove_prompt("my_prompt")
#### `add_middleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L394" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_middleware(self, middleware: Middleware) -> None
```
#### `add_provider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L507" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_provider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L397" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_provider(self, provider: Provider) -> None
@ -132,7 +141,7 @@ always take precedence over providers.
- Prompts become "namespace_promptname"
#### `get_tasks` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L529" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tasks` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L419" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tasks(self) -> Sequence[FastMCPComponent]
@ -144,7 +153,7 @@ Overrides AggregateProvider.get_tasks() to apply server-level transforms
after aggregation. AggregateProvider handles provider-level namespacing.
#### `add_transform` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L558" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_transform` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L448" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_transform(self, transform: Transform) -> None
@ -159,7 +168,7 @@ They transform tools, resources, and prompts from ALL providers.
- `transform`: The transform to add.
#### `add_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L578" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L468" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfig) -> None
@ -171,7 +180,7 @@ Add a tool transformation.
Use ``add_transform(ToolTransform({...}))`` instead.
#### `remove_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L595" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `remove_tool_transformation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L485" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
remove_tool_transformation(self, _tool_name: str) -> None
@ -183,7 +192,7 @@ Remove a tool transformation.
Tool transformations are now immutable. Use enable/disable controls instead.
#### `list_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L610" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L500" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_tools(self) -> Sequence[Tool]
@ -196,7 +205,7 @@ and middleware execution. Returns all versions (no deduplication).
Protocol handlers deduplicate for MCP wire format.
#### `get_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L680" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L570" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tool(self, name: str, version: VersionSpec | None = None) -> Tool | None
@ -216,7 +225,7 @@ session transforms can override provider-level disables.
- The tool if found and enabled, None otherwise.
#### `list_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L706" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L596" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resources(self) -> Sequence[Resource]
@ -229,7 +238,7 @@ and middleware execution. Returns all versions (no deduplication).
Protocol handlers deduplicate for MCP wire format.
#### `get_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L778" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L668" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_resource(self, uri: str, version: VersionSpec | None = None) -> Resource | None
@ -248,7 +257,7 @@ transforms (including session-level) have been applied.
- The resource if found and enabled, None otherwise.
#### `list_resource_templates` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L803" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_resource_templates` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L693" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resource_templates(self) -> Sequence[ResourceTemplate]
@ -261,7 +270,7 @@ auth filtering, and middleware execution. Returns all versions (no deduplication
Protocol handlers deduplicate for MCP wire format.
#### `get_resource_template` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L877" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_resource_template` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L767" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_resource_template(self, uri: str, version: VersionSpec | None = None) -> ResourceTemplate | None
@ -280,7 +289,7 @@ all transforms (including session-level) have been applied.
- The template if found and enabled, None otherwise.
#### `list_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L902" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `list_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L792" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_prompts(self) -> Sequence[Prompt]
@ -293,7 +302,7 @@ and middleware execution. Returns all versions (no deduplication).
Protocol handlers deduplicate for MCP wire format.
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L972" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L862" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompt(self, name: str, version: VersionSpec | None = None) -> Prompt | None
@ -312,19 +321,19 @@ transforms (including session-level) have been applied.
- The prompt if found and enabled, None otherwise.
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L998" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L888" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult
```
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1009" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L899" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.CreateTaskResult
```
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1019" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L909" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult | mcp.types.CreateTaskResult
@ -354,19 +363,19 @@ return ToolResult.
- `ValidationError`: If arguments fail validation
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1115" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1005" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource(self, uri: str) -> ResourceResult
```
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1125" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1015" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource(self, uri: str) -> mcp.types.CreateTaskResult
```
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1134" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1024" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource(self, uri: str) -> ResourceResult | mcp.types.CreateTaskResult
@ -395,19 +404,19 @@ return ResourceResult.
- `ResourceError`: If resource read fails
#### `render_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1268" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `render_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1158" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult
```
#### `render_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1279" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `render_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1169" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.CreateTaskResult
```
#### `render_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1289" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `render_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1179" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult | mcp.types.CreateTaskResult
@ -437,7 +446,7 @@ return PromptResult.
- `PromptError`: If prompt rendering fails
#### `add_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1365" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1255" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_tool(self, tool: Tool | Callable[..., Any]) -> Tool
@ -455,7 +464,7 @@ with the Context type annotation. See the @tool decorator for examples.
- The tool instance that was added to the server.
#### `remove_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1379" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `remove_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1269" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
remove_tool(self, name: str, version: str | None = None) -> None
@ -463,6 +472,9 @@ remove_tool(self, name: str, version: str | None = None) -> None
Remove tool(s) from the server.
.. deprecated::
Use ``mcp.local_provider.remove_tool(name)`` instead.
**Args:**
- `name`: The name of the tool to remove.
- `version`: If None, removes ALL versions. If specified, removes only that version.
@ -471,19 +483,19 @@ Remove tool(s) from the server.
- `NotFoundError`: If no matching tool is found.
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1399" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1299" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: AnyFunction) -> FunctionTool
```
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1420" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1320" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionTool]
```
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1440" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1340" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]
@ -539,7 +551,7 @@ server.tool(my_function, name="custom_name")
```
#### `add_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1537" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1439" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_resource(self, resource: Resource | Callable[..., Any]) -> Resource | ResourceTemplate
@ -554,7 +566,7 @@ Add a resource to the server.
- The resource instance that was added to the server.
#### `add_template` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1550" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_template` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1452" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_template(self, template: ResourceTemplate) -> ResourceTemplate
@ -569,7 +581,7 @@ Add a resource template to the server.
- The template instance that was added to the server.
#### `resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1561" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1463" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
resource(self, uri: str) -> Callable[[AnyFunction], Resource | ResourceTemplate | AnyFunction]
@ -628,7 +640,7 @@ async def get_weather(city: str) -> str:
```
#### `add_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1666" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1585" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_prompt(self, prompt: Prompt | Callable[..., Any]) -> Prompt
@ -643,19 +655,19 @@ Add a prompt to the server.
- The prompt instance that was added to the server.
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1678" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1597" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
prompt(self, name_or_fn: AnyFunction) -> FunctionPrompt
```
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1694" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1613" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
prompt(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionPrompt]
```
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1709" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1628" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
prompt(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt | partial[Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt]
@ -732,7 +744,7 @@ Decorator to register a prompt.
```
#### `mount` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1809" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `mount` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1728" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
mount(self, server: FastMCP[LifespanResultT], namespace: str | None = None, as_proxy: bool | None = None, tool_names: dict[str, str] | None = None, prefix: str | None = None) -> None
@ -779,7 +791,7 @@ mounted server.
- `prefix`: Deprecated. Use namespace instead.
#### `import_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1903" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `import_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1822" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
import_server(self, server: FastMCP[LifespanResultT], prefix: str | None = None) -> None
@ -820,10 +832,10 @@ templates, and prompts are imported with their original names.
objects are imported with their original names.
#### `from_openapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2003" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_openapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1922" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient | None = None, name: str = 'OpenAPI Server', route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, **settings: Any) -> Self
from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient | None = None, name: str = 'OpenAPI Server', route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, validate_output: bool = True, **settings: Any) -> Self
```
Create a FastMCP server from an OpenAPI specification.
@ -839,13 +851,17 @@ server URL from the OpenAPI spec with a 30-second timeout.
- `mcp_component_fn`: Optional callable for component customization
- `mcp_names`: Optional dictionary mapping operationId to component names
- `tags`: Optional set of tags to add to all components
- `validate_output`: If True (default), tools use the output schema
extracted from the OpenAPI spec for response validation. If
False, a permissive schema is used instead, allowing any
response structure while still returning structured JSON.
- `**settings`: Additional settings passed to FastMCP
**Returns:**
- A FastMCP server with an OpenAPIProvider attached.
#### `from_fastapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2048" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_fastapi` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L1973" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, httpx_client_kwargs: dict[str, Any] | None = None, tags: set[str] | None = None, **settings: Any) -> Self
@ -869,7 +885,7 @@ Use this to configure timeout and other client settings.
- A FastMCP server with an OpenAPIProvider attached.
#### `as_proxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2103" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `as_proxy` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2028" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
@ -887,7 +903,7 @@ instance or any value accepted as the `transport` argument of
`fastmcp.client.Client` constructor.
#### `generate_name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2140" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `generate_name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/server.py#L2065" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
generate_name(cls, name: str | None = None) -> str

View file

@ -13,7 +13,7 @@ in Docket workers. Unlike regular MCP requests, background tasks don't have
an active request context, so elicitation requires special handling:
1. Set task status to "input_required" via Redis
2. Send notifications/tasks/updated with elicitation metadata
2. Send notifications/tasks/status with elicitation metadata
3. Wait for client to send input via tasks/sendInput
4. Resume task execution with the provided input
@ -26,7 +26,7 @@ internal APIs for background task coordination.
### `elicit_for_task` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/tasks/elicitation.py#L42" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit_for_task(task_id: str, session: ServerSession, message: str, schema: dict[str, Any], fastmcp: FastMCP) -> mcp.types.ElicitResult
elicit_for_task(task_id: str, session: ServerSession | None, message: str, schema: dict[str, Any], fastmcp: FastMCP) -> mcp.types.ElicitResult
```
@ -50,7 +50,29 @@ in a Docket worker context where there's no active MCP request.
- `McpError`: If the elicitation request fails
### `handle_task_input` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/tasks/elicitation.py#L176" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `relay_elicitation` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/tasks/elicitation.py#L234" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
relay_elicitation(session: ServerSession, session_id: str, task_id: str, elicitation: dict[str, Any], fastmcp: FastMCP) -> None
```
Relay elicitation from a background task worker to the client.
Called by the notification subscriber when it detects an input_required
notification with elicitation metadata. Sends a standard elicitation/create
request to the client session, then uses handle_task_input() to push the
response to Redis so the blocked worker can resume.
**Args:**
- `session`: MCP ServerSession
- `session_id`: Session identifier
- `task_id`: Background task ID
- `elicitation`: Elicitation metadata (message, requestedSchema)
- `fastmcp`: FastMCP server instance
### `handle_task_input` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/tasks/elicitation.py#L290" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
handle_task_input(task_id: str, session_id: str, action: str, content: dict[str, Any] | None, fastmcp: FastMCP) -> bool

View file

@ -13,7 +13,7 @@ Handles queuing tool/prompt/resource executions to Docket as background tasks.
## Functions
### `submit_to_docket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/tasks/handlers.py#L31" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `submit_to_docket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/tasks/handlers.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
submit_to_docket(task_type: Literal['tool', 'resource', 'template', 'prompt'], key: str, component: Tool | Resource | ResourceTemplate | Prompt, arguments: dict[str, Any] | None = None, task_meta: TaskMeta | None = None) -> mcp.types.CreateTaskResult

View file

@ -0,0 +1,113 @@
---
title: notifications
sidebarTitle: notifications
---
# `fastmcp.server.tasks.notifications`
Distributed notification queue for background task events (SEP-1686).
Enables distributed Docket workers to send MCP notifications to clients
without holding session references. Workers push to a Redis queue,
the MCP server process subscribes and forwards to the client's session.
Pattern: Fire-and-forward with retry
- One queue per session_id
- LPUSH/BRPOP for reliable ordered delivery
- Retry up to 3 times on delivery failure, then discard
- TTL-based expiration for stale messages
Note: Docket's execution.subscribe() handles task state/progress events via
Redis Pub/Sub. This module handles elicitation-specific notifications that
require reliable delivery (input_required prompts, cancel signals).
## Functions
### `push_notification` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/tasks/notifications.py#L48" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
push_notification(session_id: str, notification: dict[str, Any], docket: Docket) -> None
```
Push notification to session's queue (called from Docket worker).
Used for elicitation-specific notifications (input_required, cancel)
that need reliable delivery across distributed processes.
**Args:**
- `session_id`: Target session's identifier
- `notification`: MCP notification dict (method, params, _meta)
- `docket`: Docket instance for Redis access
### `notification_subscriber_loop` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/tasks/notifications.py#L76" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
notification_subscriber_loop(session_id: str, session: ServerSession, docket: Docket, fastmcp: FastMCP) -> None
```
Subscribe to notification queue and forward to session.
Runs in the MCP server process. Bridges distributed workers to clients.
This loop:
1. Maintains a heartbeat (active subscriber marker for debugging)
2. Blocks on BRPOP waiting for notifications
3. Forwards notifications to the client's session
4. Retries failed deliveries, then discards (no dead-letter queue)
**Args:**
- `session_id`: Session identifier to subscribe to
- `session`: MCP ServerSession for sending notifications
- `docket`: Docket instance for Redis access
- `fastmcp`: FastMCP server instance (for elicitation relay)
### `ensure_subscriber_running` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/tasks/notifications.py#L238" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
ensure_subscriber_running(session_id: str, session: ServerSession, docket: Docket, fastmcp: FastMCP) -> None
```
Start notification subscriber if not already running (idempotent).
Subscriber is created on first task submission and cleaned up on disconnect.
Safe to call multiple times for the same session.
**Args:**
- `session_id`: Session identifier
- `session`: MCP ServerSession
- `docket`: Docket instance
- `fastmcp`: FastMCP server instance (for elicitation relay)
### `stop_subscriber` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/tasks/notifications.py#L278" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
stop_subscriber(session_id: str) -> None
```
Stop notification subscriber for a session.
Called when session disconnects. Pending messages remain in queue
for delivery if client reconnects (with TTL expiration).
**Args:**
- `session_id`: Session identifier
### `get_subscriber_count` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/tasks/notifications.py#L298" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_subscriber_count() -> int
```
Get number of active subscribers (for monitoring).

View file

@ -7,15 +7,13 @@ sidebarTitle: settings
## Classes
### `DocketSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L30" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `DocketSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L29" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Docket worker configuration.
### `ExperimentalSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L118" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `Settings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L142" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `Settings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L117" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
FastMCP settings.
@ -23,7 +21,7 @@ FastMCP settings.
**Methods:**
#### `get_setting` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L154" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_setting` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L129" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_setting(self, attr: str) -> Any
@ -33,7 +31,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/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L167" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `set_setting` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L142" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_setting(self, attr: str, value: Any) -> None
@ -43,7 +41,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/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L189" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `normalize_log_level` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/settings.py#L164" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
normalize_log_level(cls, v)

View file

@ -10,7 +10,7 @@ Standalone @tool decorator for FastMCP.
## Functions
### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L370" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L371" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(name_or_fn: str | Callable[..., Any] | None = None) -> Any
@ -37,11 +37,11 @@ Protocol for functions decorated with @tool.
Metadata attached to functions by the @tool decorator.
### `FunctionTool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L84" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `FunctionTool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L85" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
**Methods:**
#### `to_mcp_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L87" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `to_mcp_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L88" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
to_mcp_tool(self, **overrides: Any) -> mcp.types.Tool
@ -52,7 +52,7 @@ Convert the FastMCP tool to an MCP tool.
Extends the base implementation to add task execution mode if enabled.
#### `from_function` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_function` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L107" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_function(cls, fn: Callable[..., Any]) -> FunctionTool
@ -68,7 +68,7 @@ individual parameters must not be passed.
Cannot be used together with metadata parameter.
#### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L248" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L249" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run(self, arguments: dict[str, Any]) -> ToolResult
@ -77,7 +77,7 @@ run(self, arguments: dict[str, Any]) -> ToolResult
Run the tool with arguments.
#### `register_with_docket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L293" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `register_with_docket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L294" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
register_with_docket(self, docket: Docket) -> None
@ -89,7 +89,7 @@ FunctionTool registers the underlying function, which has the user's
Depends parameters for docket to resolve.
#### `add_to_docket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L303" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_to_docket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/function_tool.py#L304" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_to_docket(self, docket: Docket, arguments: dict[str, Any], **kwargs: Any) -> Execution

View file

@ -7,7 +7,7 @@ sidebarTitle: tool
## Functions
### `default_serializer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L59" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `default_serializer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L56" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
default_serializer(data: Any) -> str
@ -15,17 +15,17 @@ default_serializer(data: Any) -> str
## Classes
### `ToolResult` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ToolResult` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L60" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
**Methods:**
#### `to_mcp_result` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L108" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `to_mcp_result` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L105" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
to_mcp_result(self) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]] | CallToolResult
```
### `Tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L124" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `Tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L121" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Internal tool registration info.
@ -33,7 +33,7 @@ Internal tool registration info.
**Methods:**
#### `to_mcp_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L166" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `to_mcp_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L163" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
to_mcp_tool(self, **overrides: Any) -> MCPTool
@ -42,7 +42,7 @@ to_mcp_tool(self, **overrides: Any) -> MCPTool
Convert the FastMCP tool to an MCP tool.
#### `from_function` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L193" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_function` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L190" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_function(cls, fn: Callable[..., Any]) -> FunctionTool
@ -51,7 +51,7 @@ from_function(cls, fn: Callable[..., Any]) -> FunctionTool
Create a Tool from a function.
#### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L233" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L230" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run(self, arguments: dict[str, Any]) -> ToolResult
@ -66,7 +66,7 @@ implemented by subclasses.
(list of ContentBlocks, dict of structured output).
#### `convert_result` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L245" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `convert_result` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L242" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
convert_result(self, raw_value: Any) -> ToolResult
@ -78,7 +78,7 @@ Handles ToolResult passthrough and converts raw values using the tool's
attributes (serializer, output_schema) for proper conversion.
#### `register_with_docket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L337" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `register_with_docket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L334" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
register_with_docket(self, docket: Docket) -> None
@ -87,7 +87,7 @@ register_with_docket(self, docket: Docket) -> None
Register this tool with docket for background execution.
#### `add_to_docket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L343" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_to_docket` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L340" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_to_docket(self, docket: Docket, arguments: dict[str, Any], **kwargs: Any) -> Execution
@ -103,13 +103,13 @@ Schedule this tool for background execution via docket.
- `**kwargs`: Additional kwargs passed to docket.add()
#### `from_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L367" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L364" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_tool(cls, tool: Tool) -> TransformedTool
```
#### `get_span_attributes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L398" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `get_span_attributes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/tools/tool.py#L395" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_span_attributes(self) -> dict[str, Any]

View file

@ -60,17 +60,12 @@ the referenced definition while preserving $defs for nested references.
### `compress_schema` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/json_schema.py#L364" 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) -> dict[str, Any]
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]
```
Compress and optimize a JSON schema for MCP compatibility.
This function dereferences all $ref entries (inlining definitions) to ensure
compatibility with MCP clients that don't properly handle $ref in schemas
(e.g., VS Code Copilot). It also applies various optimizations to reduce
schema size.
**Args:**
- `schema`: The schema to compress
- `prune_params`: List of parameter names to remove from properties
@ -78,4 +73,7 @@ schema size.
Defaults to False to maintain MCP client compatibility, as some clients
(e.g., Claude) require additionalProperties\: false for strict validation.
- `prune_titles`: Whether to remove title fields from the schema
- `dereference`: Whether to dereference $ref by inlining definitions.
Defaults to False; dereferencing is typically handled by
middleware at serve-time instead.

View file

@ -72,26 +72,7 @@ format_json_for_description(data: Any, indent: int = 2) -> str
Formats Python data as a JSON string block for Markdown.
### `format_simple_description` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/openapi/formatters.py#L192" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
format_simple_description(base_description: str, parameters: list[ParameterInfo] | None = None, request_body: RequestBodyInfo | None = None) -> str
```
Formats a simple description for MCP objects (tools, resources, prompts).
Excludes response details, examples, and verbose status codes.
**Args:**
- `base_description`: The initial description to be formatted.
- `parameters`: A list of parameter information.
- `request_body`: Information about the request body.
**Returns:**
- The formatted description string with minimal details.
### `format_description_with_responses` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/openapi/formatters.py#L225" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `format_description_with_responses` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/openapi/formatters.py#L192" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
format_description_with_responses(base_description: str, responses: dict[str, Any], parameters: list[ParameterInfo] | None = None, request_body: RequestBodyInfo | None = None) -> str

View file

@ -24,7 +24,7 @@ When an `AuthProvider` is configured, all requests to the MCP endpoint must carr
## Auth Checks
An auth check is any callable that accepts an `AuthContext` and returns a boolean. The `AuthContext` provides access to the current token (if any) and the component being accessed.
An auth check is any callable that accepts an `AuthContext` and returns a boolean. Auth checks can be synchronous or asynchronous, so checks that need to perform async operations (like reading server state or calling external services) work naturally.
```python
from fastmcp.server.auth import AuthContext
@ -137,6 +137,34 @@ def advanced_feature() -> str:
return "Advanced feature"
```
### Async Auth Checks
Auth checks can be `async` functions, which is useful when the authorization decision depends on asynchronous operations like reading server state or querying external services.
```python
from fastmcp import FastMCP
from fastmcp.server.auth import AuthContext
mcp = FastMCP("Async Auth Server")
async def check_user_permissions(ctx: AuthContext) -> bool:
"""Async auth check that reads server state."""
if ctx.token is None:
return False
user_id = ctx.token.claims.get("sub")
# Async operations work naturally in auth checks
permissions = await fetch_user_permissions(user_id)
return "admin" in permissions
@mcp.tool(auth=check_user_permissions)
def admin_tool() -> str:
return "Admin action completed"
```
Sync and async checks can be freely combined in a list — each check is handled according to its type.
### Error Handling
Auth checks can raise exceptions for explicit denial with custom messages:
- **`AuthorizationError`**: Propagates with its custom message, useful for explaining why access was denied
@ -346,7 +374,7 @@ def require_matching_tag(ctx: AuthContext) -> bool:
from fastmcp.server.auth import (
AccessToken, # Token with .token, .client_id, .scopes, .expires_at, .claims
AuthContext, # Context with .token, .component
AuthCheck, # Type alias: Callable[[AuthContext], bool]
AuthCheck, # Type alias: sync or async Callable[[AuthContext], bool]
require_scopes, # Built-in: requires specific scopes
restrict_tag, # Built-in: tag-based scope requirements
run_auth_checks, # Utility: run checks with AND logic

View file

@ -238,14 +238,32 @@ async def get_counter(ctx: Context) -> int:
Each client session has its own isolated state—two different clients calling `increment_counter` will each have their own counter.
**Method signatures:**
- **`await ctx.set_state(key: str, value: Any) -> None`**: Store a value in session state
- **`await ctx.get_state(key: str) -> Any`**: Retrieve a value (returns None if not found)
- **`await ctx.delete_state(key: str) -> None`**: Remove a value from session state
- **`await ctx.set_state(key, value, *, serializable=True)`**: Store a value in session state
- **`await ctx.get_state(key)`**: Retrieve a value (returns None if not found)
- **`await ctx.delete_state(key)`**: Remove a value from session state
<Note>
State methods are async and require `await`. State expires after 1 day to prevent unbounded memory growth.
</Note>
#### Non-Serializable Values
By default, state values must be JSON-serializable (dicts, lists, strings, numbers, etc.) so they can be persisted across requests. For non-serializable values like HTTP clients or database connections, pass `serializable=False`:
```python
@mcp.tool
async def my_tool(ctx: Context) -> str:
# This object can't be JSON-serialized
client = SomeHTTPClient(base_url="https://api.example.com")
await ctx.set_state("client", client, serializable=False)
# Retrieve it later in the same request
client = await ctx.get_state("client")
return await client.fetch("/data")
```
Values stored with `serializable=False` only live for the current MCP request (a single tool call, resource read, or prompt render). They will not be available in subsequent requests within the session.
#### Custom Storage Backends
By default, session state uses an in-memory store suitable for single-server deployments. For distributed or serverless deployments, provide a custom storage backend:

View file

@ -237,6 +237,37 @@ The `AccessToken` object provides:
- **`expires_at`**: Token expiration timestamp (if available)
- **`claims`**: Dictionary of all token claims (JWT claims or provider-specific data)
### Token Claims
When you need just one specific value from the token—like a user ID or tenant identifier—`TokenClaim()` extracts it directly without needing the full token object.
```python
from fastmcp import FastMCP
from fastmcp.server.dependencies import TokenClaim
mcp = FastMCP("Demo")
@mcp.tool
async def add_expense(
amount: float,
user_id: str = TokenClaim("oid"), # Azure object ID
) -> dict:
await db.insert({"user_id": user_id, "amount": amount})
return {"status": "created", "user_id": user_id}
```
`TokenClaim()` raises a `RuntimeError` if the claim doesn't exist, listing available claims to help with debugging.
Common claims vary by identity provider:
| Provider | User ID Claim | Email Claim | Name Claim |
|----------|--------------|-------------|------------|
| Azure/Entra | `oid` | `email` | `name` |
| GitHub | `sub` | `email` | `name` |
| Google | `sub` | `email` | `name` |
| Auth0 | `sub` | `email` | `name` |
### Background Task Dependencies
<VersionBadge version="2.3.0" />

View file

@ -81,9 +81,9 @@ mcp.add_prompt(my_prompt)
Remove components by name or URI:
```python
mcp.remove_tool("my_tool")
mcp.remove_resource("data://info")
mcp.remove_prompt("my_prompt")
mcp.local_provider.remove_tool("my_tool")
mcp.local_provider.remove_resource("data://info")
mcp.local_provider.remove_prompt("my_prompt")
```
## Duplicate Handling

View file

@ -289,6 +289,45 @@ def search(query: str) -> str:
`ToolError` messages always pass through to the LLM, making it the escape hatch for errors you want the LLM to see and handle.
### Concurrent Tool Execution
By default, tools execute sequentially — one at a time, in order. When your tools are independent (no shared state between them), you can execute them in parallel with `tool_concurrency`:
```python
result = await ctx.sample(
messages="Research these three topics",
tools=[search, fetch_url],
tool_concurrency=0, # Unlimited parallel execution
)
```
The `tool_concurrency` parameter controls how many tools run at once:
- **`None`** (default): Sequential execution
- **`0`**: Unlimited parallel execution
- **`N > 0`**: Execute at most N tools concurrently
For tools that must not run concurrently (file writes, shared state mutations, etc.), mark them as `sequential` when creating the `SamplingTool`:
```python
from fastmcp.server.sampling import SamplingTool
db_writer = SamplingTool.from_function(
write_to_db,
sequential=True, # Forces all tools in the batch to run sequentially
)
result = await ctx.sample(
messages="Process this data",
tools=[search, db_writer],
tool_concurrency=0, # Would be parallel, but db_writer forces sequential
)
```
<Note>
When any tool in a batch has `sequential=True`, the entire batch executes sequentially regardless of `tool_concurrency`. This is a conservative guarantee — if one tool needs ordering, all tools in that batch respect it.
</Note>
### Client Requirements
<Note>
@ -463,6 +502,10 @@ tool_result = ToolResultContent(
If True, mask detailed error messages from tool execution. When None (default), uses the global `settings.mask_error_details` value. Tools can raise `ToolError` to bypass masking and provide specific error messages to the LLM.
</ResponseField>
<ResponseField name="tool_concurrency" type="int | None" default="None">
Controls parallel execution of tools. `None` (default) for sequential, `0` for unlimited parallel, or a positive integer for bounded concurrency. If any tool has `sequential=True`, all tools execute sequentially regardless.
</ResponseField>
</Expandable>
<Expandable title="Response">
@ -511,6 +554,10 @@ tool_result = ToolResultContent(
<ResponseField name="mask_error_details" type="bool | None" default="None">
If True, mask detailed error messages from tool execution.
</ResponseField>
<ResponseField name="tool_concurrency" type="int | None" default="None">
Controls parallel execution of tools. `None` (default) for sequential, `0` for unlimited parallel, or a positive integer for bounded concurrency.
</ResponseField>
</Expandable>
<Expandable title="Response">

View file

@ -43,7 +43,7 @@ MCP background tasks are different: they're **protocol-native**. This means MCP
<VersionBadge version="3.0.0" /> Background tasks require the `tasks` extra:
```bash
pip install "fastmcp[tasks]>=3.0.0b2"
pip install "fastmcp[tasks]>=3.0.0rc1"
```
Add `task=True` to any tool, resource, resource template, or prompt decorator. This marks the component as capable of background execution.

View file

@ -175,6 +175,12 @@ By default, FastMCP converts Python functions into MCP tools by inspecting the f
<Note>
FastMCP automatically dereferences `$ref` entries in tool schemas to ensure compatibility with MCP clients that don't fully support JSON Schema references (e.g., VS Code Copilot, Claude Desktop). This means complex Pydantic models with shared types are inlined in the schema rather than using `$defs` references.
Dereferencing happens at serve-time via middleware, so your schemas are stored with `$ref` intact and only inlined when sent to clients. If you know your clients handle `$ref` correctly and prefer smaller schemas, you can opt out:
```python
mcp = FastMCP("my-server", dereference_schemas=False)
```
</Note>
### Type Annotations
@ -969,7 +975,7 @@ def example_tool() -> str:
mcp.add_tool(example_tool) # Sends tools/list_changed notification
mcp.disable(keys={"tool:example_tool"}) # Sends tools/list_changed notification
mcp.enable(keys={"tool:example_tool"}) # Sends tools/list_changed notification
mcp.remove_tool("example_tool") # Sends tools/list_changed notification
mcp.local_provider.remove_tool("example_tool") # Sends tools/list_changed notification
```
Notifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications.
@ -1054,7 +1060,7 @@ The duplicate behavior options are:
<VersionBadge version="2.3.4" />
You can dynamically remove tools from a server using the `remove_tool` method:
You can dynamically remove tools from a server through its [local provider](/servers/providers/local):
```python
from fastmcp import FastMCP
@ -1066,7 +1072,7 @@ def calculate_sum(a: int, b: int) -> int:
"""Add two numbers together."""
return a + b
mcp.remove_tool("calculate_sum")
mcp.local_provider.remove_tool("calculate_sum")
```
## Versioning

View file

@ -293,14 +293,14 @@ If the requested version doesn't exist, a `NotFoundError` is raised.
## Removing Versions
The `remove_tool`, `remove_resource`, and `remove_prompt` methods accept an optional `version` parameter that controls what gets removed.
The `remove_tool`, `remove_resource`, and `remove_prompt` methods on the server's [local provider](/servers/providers/local) accept an optional `version` parameter that controls what gets removed.
```python
# Remove ALL versions of a component
mcp.remove_tool("calculate")
mcp.local_provider.remove_tool("calculate")
# Remove only a specific version
mcp.remove_tool("calculate", version="1.0")
mcp.local_provider.remove_tool("calculate", version="1.0")
```
When you remove a specific version, other versions remain registered. When you remove without specifying a version, all versions are removed.
@ -332,5 +332,5 @@ Clients automatically see version 2.0 (the highest). During the transition, your
Once the migration is complete, remove the old version.
```python
mcp.remove_tool("process_data", version="1.0")
mcp.local_provider.remove_tool("process_data", version="1.0")
```

View file

@ -5,6 +5,48 @@ icon: "sparkles"
tag: NEW
---
<Update label="FastMCP 3.0.0rc1" description="February 12, 2026" tags={["Releases"]}>
<Card
title="FastMCP v3.0.0rc1: RC-ing is Believing"
href="https://github.com/jlowin/fastmcp/releases/tag/v3.0.0rc1"
cta="Read the release notes"
>
FastMCP 3 RC1 means we believe the API is stable. Beta 2 drew a wave of real-world adoption — production deployments, migration reports, integration testing — and the feedback overwhelmingly confirmed that the architecture works. This release closes gaps that surfaced under load: auth flows that needed to be async, background tasks that needed reliable notification delivery, and APIs still carrying beta-era naming. If nothing unexpected surfaces, this is what 3.0.0 looks like.
🚨 **Breaking Changes** — The `ui=` parameter is now `app=` with a unified `AppConfig` class, and 16 `FastMCP()` constructor kwargs have been removed after months of deprecation warnings.
🔐 **Auth Improvements** — Async `auth=` checks, Static Client Registration for servers without DCR, and declarative Azure OBO flows via dependency injection.
⚡ **Concurrent Sampling** — `context.sample()` can now execute multiple tool calls in parallel with `tool_concurrency=0`.
📡 **Background Task Notifications** — A distributed Redis queue replaces polling for progress updates and elicitation relay.
✅ **OpenAPI Output Validation** — `validate_output=False` disables strict schema checking for imperfect backend APIs.
</Card>
</Update>
<Update label="FastMCP 3.0.0b2" description="February 7, 2026" tags={["Releases"]}>
<Card
title="FastMCP v3.0.0b2: 2 Fast 2 Beta"
href="https://github.com/jlowin/fastmcp/releases/tag/v3.0.0b2"
cta="Read the release notes"
>
Beta 2 reflects the huge number of people that kicked the tires on Beta 1. Seven new contributors landed changes, and early migration reports went smoother than expected. Most of Beta 2 is refinement — fixing what people found, filling gaps from real usage, hardening edges — but a few new features landed along the way.
🖥️ **Client CLI** — `fastmcp list`, `fastmcp call`, `fastmcp discover`, and `fastmcp generate-cli` turn any MCP server into something you can poke at from a terminal.
🔐 **CIMD** (Client ID Metadata Documents) adds an alternative to Dynamic Client Registration for OAuth.
📱 **MCP Apps** — Spec-level compliance for the MCP Apps extension with `ui://` resource scheme and typed UI metadata.
⏳ **Background Task Context** — `Context` now works transparently in Docket workers with Redis-based coordination.
🛡️ **ResponseLimitingMiddleware** caps tool response sizes with UTF-8-safe truncation.
🪿 **Goose Integration** — `fastmcp install goose` for one-command server installation into Goose.
</Card>
</Update>
<Update label="FastMCP 3.0.0b1" description="January 20, 2026" tags={["Releases"]}>
<Card
title="FastMCP 3.0.0b1: This Beta Work"

View file

@ -12,7 +12,6 @@ Usage:
from __future__ import annotations
from prefab_ui import UIResponse
from prefab_ui.components import (
BarChart,
ChartSeries,
@ -21,6 +20,7 @@ from prefab_ui.components import (
LineChart,
Muted,
)
from prefab_ui.response import UIResponse
from fastmcp import FastMCP

View file

@ -13,7 +13,6 @@ Usage:
from __future__ import annotations
from prefab_ui import UIResponse
from prefab_ui.components import (
Badge,
Column,
@ -23,6 +22,7 @@ from prefab_ui.components import (
Muted,
Row,
)
from prefab_ui.response import UIResponse
from fastmcp import FastMCP

View file

@ -71,9 +71,9 @@ app.mount(server=news_app, prefix="news")
async def get_server_details():
"""Print information about mounted resources."""
# Print available tools
tools = await app.get_tools()
tools = await app.list_tools()
print(f"\nAvailable tools ({len(tools)}):")
for _, tool in tools.items():
for tool in tools:
print(f" - {tool.name}: {tool.description}")
# Print available resources
@ -82,20 +82,20 @@ async def get_server_details():
# Distinguish between native and imported resources
# Native resources would be those directly in the main app (not prefixed)
resources = await app.get_resources()
resources = await app.list_resources()
native_resources = [
uri
for uri, _ in resources.items()
if urlparse(uri).netloc not in ("weather", "news")
str(r.uri)
for r in resources
if urlparse(str(r.uri)).netloc not in ("weather", "news")
]
# Imported resources - categorized by source app
weather_resources = [
uri for uri, _ in resources.items() if urlparse(uri).netloc == "weather"
str(r.uri) for r in resources if urlparse(str(r.uri)).netloc == "weather"
]
news_resources = [
uri for uri, _ in resources.items() if urlparse(uri).netloc == "news"
str(r.uri) for r in resources if urlparse(str(r.uri)).netloc == "news"
]
print(f" - Native app resources: {native_resources}")

View file

@ -60,11 +60,11 @@ async def main():
],
)
tools = await mcp1.get_tools()
resources = await mcp1.get_resources()
tools = await mcp1.list_tools()
resources = await mcp1.list_resources()
print(f"Tools ({len(tools)}): {', '.join(tools.keys())}")
print(f"Resources ({len(resources)}): {', '.join(resources.keys())}")
print(f"Tools ({len(tools)}): {', '.join(t.name for t in tools)}")
print(f"Resources ({len(resources)}): {', '.join(str(r.uri) for r in resources)}")
print("\n=== Example 2: Exclude internal routes ===")
@ -80,11 +80,11 @@ async def main():
],
)
tools = await mcp2.get_tools()
resources = await mcp2.get_resources()
tools = await mcp2.list_tools()
resources = await mcp2.list_resources()
print(f"Tools ({len(tools)}): {', '.join(tools.keys())}")
print(f"Resources ({len(resources)}): {', '.join(resources.keys())}")
print(f"Tools ({len(tools)}): {', '.join(t.name for t in tools)}")
print(f"Resources ({len(resources)}): {', '.join(str(r.uri) for r in resources)}")
print("\n=== Example 3: Pattern + Tags combination ===")
@ -107,11 +107,11 @@ async def main():
],
)
tools = await mcp3.get_tools()
resources = await mcp3.get_resources()
tools = await mcp3.list_tools()
resources = await mcp3.list_resources()
print(f"Tools ({len(tools)}): {', '.join(tools.keys())}")
print(f"Resources ({len(resources)}): {', '.join(resources.keys())}")
print(f"Tools ({len(tools)}): {', '.join(t.name for t in tools)}")
print(f"Resources ({len(resources)}): {', '.join(str(r.uri) for r in resources)}")
print("\n=== Example 4: Multiple tag AND condition ===")
@ -130,11 +130,11 @@ async def main():
],
)
tools = await mcp4.get_tools()
resources = await mcp4.get_resources()
tools = await mcp4.list_tools()
resources = await mcp4.list_resources()
print(f"Tools ({len(tools)}): {', '.join(tools.keys())}")
print(f"Resources ({len(resources)}): {', '.join(resources.keys())}")
print(f"Tools ({len(tools)}): {', '.join(t.name for t in tools)}")
print(f"Resources ({len(resources)}): {', '.join(str(r.uri) for r in resources)}")
if __name__ == "__main__":

View file

@ -0,0 +1,82 @@
"""
Background task elicitation demo.
A background task (Docket) that pauses mid-execution to ask the user a
question, waits for the answer, then resumes and finishes.
Works with both in-memory and Redis backends:
# In-memory (single process, no Redis needed)
FASTMCP_DOCKET_URL=memory:// uv run python examples/task_elicitation.py
# Redis (distributed, needs a worker running separately)
# Terminal 1: docker compose -f examples/tasks/docker-compose.yml up -d
# Terminal 2: FASTMCP_DOCKET_URL=redis://localhost:24242/0 \
# uv run fastmcp tasks worker examples/task_elicitation.py
# Terminal 3: FASTMCP_DOCKET_URL=redis://localhost:24242/0 \
# uv run python examples/task_elicitation.py
Requires the `docket` extra (included in dev dependencies).
"""
import asyncio
from dataclasses import dataclass
from mcp.types import TextContent
from fastmcp import Context, FastMCP
from fastmcp.client import Client
from fastmcp.server.elicitation import AcceptedElicitation
mcp = FastMCP("Task Elicitation Demo")
@dataclass
class DinnerPrefs:
cuisine: str
vegetarian: bool
@mcp.tool(task=True)
async def plan_dinner(ctx: Context) -> str:
"""Plan a dinner menu, asking the user what they're in the mood for."""
await ctx.report_progress(0, 2, "Asking what you'd like...")
result = await ctx.elicit(
"What kind of dinner are you in the mood for?",
response_type=DinnerPrefs,
)
if not isinstance(result, AcceptedElicitation):
return "Dinner cancelled!"
prefs = result.data
await ctx.report_progress(1, 2, "Planning your menu...")
await asyncio.sleep(1)
await ctx.report_progress(2, 2, "Done!")
veg = "vegetarian " if prefs.vegetarian else ""
return f"Tonight's menu: a lovely {veg}{prefs.cuisine} dinner!"
async def handle_elicitation(message, response_type, params, context):
"""Handle elicitation requests from background tasks."""
print(f" Server asks: {message}")
print(" Responding with: cuisine=Thai, vegetarian=True")
return DinnerPrefs(cuisine="Thai", vegetarian=True)
async def main():
async with Client(mcp, elicitation_handler=handle_elicitation) as client:
print("Starting background task...")
task = await client.call_tool("plan_dinner", {}, task=True)
print(f" task_id = {task.task_id}\n")
result = await task.result()
assert isinstance(result.content[0], TextContent)
print(f"\nResult: {result.content[0].text}")
if __name__ == "__main__":
asyncio.run(main())

View file

@ -304,67 +304,62 @@ wheels = [
[[package]]
name = "cryptography"
version = "46.0.3"
version = "46.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
{ url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
{ url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
{ url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
{ url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
{ url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
{ url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
{ url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
{ url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
{ url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
{ url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
{ url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
{ url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
{ url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
{ url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
{ url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" },
{ url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" },
{ url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" },
{ url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" },
{ url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" },
{ url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" },
{ url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" },
{ url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" },
{ url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" },
{ url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" },
{ url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" },
{ url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" },
{ url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" },
{ url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" },
{ url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
{ url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
{ url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
{ url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
{ url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
{ url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
{ url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
{ url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
{ url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
{ url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
{ url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
{ url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
{ url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
{ url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
{ url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" },
{ url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" },
{ url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" },
{ url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" },
{ url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" },
{ url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" },
{ url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" },
{ url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
{ url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
{ url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
{ url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
{ url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
{ url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
{ url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
{ url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
{ url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
{ url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
{ url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
{ url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
{ url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
{ url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
{ url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" },
{ url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" },
{ url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" },
{ url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" },
{ url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" },
{ url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" },
{ url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" },
{ url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" },
{ url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" },
{ url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" },
{ url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" },
{ url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" },
{ url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" },
{ url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" },
{ url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
{ url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
{ url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
{ url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
{ url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
{ url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
{ url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
{ url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
{ url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
{ url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
{ url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
{ url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
{ url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
{ url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
{ url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" },
{ url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" },
{ url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" },
{ url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" },
{ url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" },
{ url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" },
]
[[package]]

View file

@ -18,7 +18,7 @@ dependencies = [
"pydantic[email]>=2.11.7",
"pyyaml>=6.0,<7.0",
"pyperclip>=1.9.0",
"py-key-value-aio[disk,keyring,memory]>=0.3.0,<0.4.0",
"py-key-value-aio[disk,keyring,memory]>=0.4.0,<0.5.0",
"uvicorn>=0.35",
"websockets>=15.0.1",
"jsonschema-path>=0.3.4",
@ -53,13 +53,14 @@ classifiers = [
[project.optional-dependencies]
anthropic = ["anthropic>=0.40.0"]
apps = ["prefab-ui>=0.1.0"]
azure = ["azure-identity>=1.16.0"]
openai = ["openai>=1.102.0"]
tasks = ["pydocket>=0.17.2"]
[dependency-groups]
dev = [
"dirty-equals>=0.9.0",
"fastmcp[anthropic,apps,openai,tasks]",
"fastmcp[anthropic,apps,azure,openai,tasks]",
# add optional dependencies for fastmcp dev
"fastapi>=0.115.12",
"opentelemetry-sdk>=1.20.0",

View file

@ -154,7 +154,12 @@ class OAuth(OAuthClientProvider):
additional_client_metadata: dict[str, Any] | None = None,
callback_port: int | None = None,
httpx_client_factory: McpHttpClientFactory | None = None,
# Alternative to dynamic client registration:
# --- Clients host a static JSON document at an HTTPS URL ---
client_metadata_url: str | None = None,
# --- OR clients provide full client information ---
client_id: str | None = None,
client_secret: str | None = None,
):
"""
Initialize OAuth client provider for an MCP server.
@ -173,6 +178,9 @@ class OAuth(OAuthClientProvider):
provided, this URL is used as the client_id instead of performing
Dynamic Client Registration. Must be an HTTPS URL with a non-root
path (e.g. "https://myapp.example.com/oauth/client.json").
client_id: Pre-registered OAuth client ID. When provided, skips dynamic
client registration and uses these static credentials instead.
client_secret: OAuth client secret (optional, used with client_id)
"""
# Store config for deferred binding if mcp_url not yet known
self._scopes = scopes
@ -181,6 +189,9 @@ class OAuth(OAuthClientProvider):
self._additional_client_metadata = additional_client_metadata
self._callback_port = callback_port
self._client_metadata_url = client_metadata_url
self._client_id = client_id
self._client_secret = client_secret
self._static_client_info = None
self.httpx_client_factory = httpx_client_factory or httpx.AsyncClient
self._bound = False
@ -218,6 +229,23 @@ class OAuth(OAuthClientProvider):
**(self._additional_client_metadata or {}),
)
if self._client_id:
# Create the full static client info directly which will avoid DCR.
# Spread client_metadata so redirect_uris, grant_types, response_types,
# scope, etc. are included — servers may validate these fields.
metadata = client_metadata.model_dump(exclude_none=True)
# Default token_endpoint_auth_method based on whether a secret is
# provided, unless the caller already set it via additional_client_metadata.
if "token_endpoint_auth_method" not in metadata:
metadata["token_endpoint_auth_method"] = (
"client_secret_post" if self._client_secret else "none"
)
self._static_client_info = OAuthClientInformationFull(
client_id=self._client_id,
client_secret=self._client_secret,
**metadata,
)
token_storage = self._token_storage or MemoryStore()
if isinstance(token_storage, MemoryStore):
@ -230,6 +258,7 @@ class OAuth(OAuthClientProvider):
stacklevel=2,
)
# Use full URL for token storage to properly separate tokens per MCP endpoint
self.token_storage_adapter: TokenStorageAdapter = TokenStorageAdapter(
async_key_value=token_storage, server_url=mcp_url
)
@ -249,10 +278,12 @@ class OAuth(OAuthClientProvider):
async def _initialize(self) -> None:
"""Load stored tokens and client info, properly setting token expiry."""
# Call parent's _initialize to load tokens and client info
await super()._initialize()
# If tokens were loaded and have expires_in, update the context's token_expiry_time
if self._static_client_info is not None:
self.context.client_info = self._static_client_info
await self.token_storage_adapter.set_client_info(self._static_client_info)
if self.context.current_tokens and self.context.current_tokens.expires_in:
self.context.update_token_expiry(self.context.current_tokens)
@ -342,6 +373,15 @@ class OAuth(OAuthClientProvider):
break
except ClientNotFoundError:
# Static credentials are fixed — retrying won't help. Surface the
# error so the user can correct their client_id / client_secret.
if self._static_client_info is not None:
raise ClientNotFoundError(
"OAuth server rejected the static client credentials. "
"Verify that the client_id (and client_secret, if provided) "
"are correct and that the client is registered with the server."
) from None
logger.debug(
"OAuth client not found on server, clearing cache and retrying..."
)

View file

@ -70,12 +70,20 @@ class ClientPromptsMixin:
"""
all_prompts: list[mcp.types.Prompt] = []
cursor: str | None = None
seen_cursors: set[str] = set()
while True:
result = await self.list_prompts_mcp(cursor=cursor)
all_prompts.extend(result.prompts)
if result.nextCursor is None:
if not result.nextCursor:
break
if result.nextCursor in seen_cursors:
logger.warning(
f"[{self.name}] Server returned duplicate pagination cursor"
f" {result.nextCursor!r} for list_prompts; stopping pagination"
)
break
seen_cursors.add(result.nextCursor)
cursor = result.nextCursor
return all_prompts

View file

@ -69,12 +69,20 @@ class ClientResourcesMixin:
"""
all_resources: list[mcp.types.Resource] = []
cursor: str | None = None
seen_cursors: set[str] = set()
while True:
result = await self.list_resources_mcp(cursor=cursor)
all_resources.extend(result.resources)
if result.nextCursor is None:
if not result.nextCursor:
break
if result.nextCursor in seen_cursors:
logger.warning(
f"[{self.name}] Server returned duplicate pagination cursor"
f" {result.nextCursor!r} for list_resources; stopping pagination"
)
break
seen_cursors.add(result.nextCursor)
cursor = result.nextCursor
return all_resources
@ -119,12 +127,21 @@ class ClientResourcesMixin:
"""
all_templates: list[mcp.types.ResourceTemplate] = []
cursor: str | None = None
seen_cursors: set[str] = set()
while True:
result = await self.list_resource_templates_mcp(cursor=cursor)
all_templates.extend(result.resourceTemplates)
if result.nextCursor is None:
if not result.nextCursor:
break
if result.nextCursor in seen_cursors:
logger.warning(
f"[{self.name}] Server returned duplicate pagination cursor"
f" {result.nextCursor!r} for list_resource_templates;"
" stopping pagination"
)
break
seen_cursors.add(result.nextCursor)
cursor = result.nextCursor
return all_templates

View file

@ -73,12 +73,20 @@ class ClientToolsMixin:
"""
all_tools: list[mcp.types.Tool] = []
cursor: str | None = None
seen_cursors: set[str] = set()
while True:
result = await self.list_tools_mcp(cursor=cursor)
all_tools.extend(result.tools)
if result.nextCursor is None:
if not result.nextCursor:
break
if result.nextCursor in seen_cursors:
logger.warning(
f"[{self.name}] Server returned duplicate pagination cursor"
f" {result.nextCursor!r} for list_tools; stopping pagination"
)
break
seen_cursors.add(result.nextCursor)
cursor = result.nextCursor
return all_tools

View file

@ -26,6 +26,7 @@ from fastmcp.server.dependencies import (
CurrentWorker,
Progress,
ProgressLike,
TokenClaim,
)
__all__ = [
@ -39,4 +40,5 @@ __all__ = [
"Depends",
"Progress",
"ProgressLike",
"TokenClaim",
]

View file

@ -10,7 +10,6 @@ from fastmcp.utilities.openapi import (
RequestBodyInfo,
ResponseInfo,
extract_output_schema_from_responses,
format_simple_description,
parse_openapi_to_http_routes,
_combine_schemas,
)
@ -32,6 +31,5 @@ __all__ = [
"ResponseInfo",
"_combine_schemas",
"extract_output_schema_from_responses",
"format_simple_description",
"parse_openapi_to_http_routes",
]

View file

@ -76,7 +76,7 @@ def infer_transport_type_from_url(
class _TransformingMCPServerMixin(FastMCPBaseModel):
"""A mixin that enables wrapping an MCP Server with tool transforms."""
tools: dict[str, ToolTransformConfig] = Field(...)
tools: dict[str, ToolTransformConfig] = Field(default_factory=dict)
"""The multi-tool transform to apply to the tools."""
include_tags: set[str] | None = Field(
@ -89,6 +89,27 @@ class _TransformingMCPServerMixin(FastMCPBaseModel):
description="The tags to exclude in the proxy.",
)
@model_validator(mode="before")
@classmethod
def _require_at_least_one_transform_field(
cls, values: dict[str, Any]
) -> dict[str, Any]:
"""Reject if none of the transforming fields are set.
This ensures that plain server configs (without tools, include_tags,
or exclude_tags) fall through to the base server types during union
validation, avoiding unnecessary proxy wrapping.
"""
if isinstance(values, dict):
has_tools = bool(values.get("tools"))
has_include = values.get("include_tags") is not None
has_exclude = values.get("exclude_tags") is not None
if not (has_tools or has_include or has_exclude):
raise ValueError(
"At least one of 'tools', 'include_tags', or 'exclude_tags' is required"
)
return values
def _to_server_and_underlying_transport(
self,
server_name: str | None = None,
@ -109,10 +130,13 @@ class _TransformingMCPServerMixin(FastMCPBaseModel):
wrapped_mcp_server = create_proxy(
client,
name=server_name,
include_tags=self.include_tags,
exclude_tags=self.exclude_tags,
)
if self.include_tags is not None:
wrapped_mcp_server.enable(tags=self.include_tags, only=True)
if self.exclude_tags is not None:
wrapped_mcp_server.disable(tags=self.exclude_tags)
# Apply tool transforms if configured
if self.tools:
from fastmcp.server.transforms import ToolTransform

View file

@ -25,12 +25,12 @@ import fastmcp
from fastmcp.decorators import resolve_task_config
from fastmcp.exceptions import PromptError
from fastmcp.prompts.prompt import Prompt, PromptArgument, PromptResult
from fastmcp.server.auth.authorization import AuthCheck
from fastmcp.server.dependencies import (
transform_context_annotations,
without_injected_parameters,
)
from fastmcp.server.tasks.config import TaskConfig
from fastmcp.tools.tool import AuthCheckCallable
from fastmcp.utilities.async_utils import call_sync_fn_in_threadpool
from fastmcp.utilities.json_schema import compress_schema
from fastmcp.utilities.logging import get_logger
@ -67,7 +67,7 @@ class PromptMeta:
tags: set[str] | None = None
meta: dict[str, Any] | None = None
task: bool | TaskConfig | None = None
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None
auth: AuthCheck | list[AuthCheck] | None = None
enabled: bool = True
@ -91,7 +91,7 @@ class FunctionPrompt(Prompt):
tags: set[str] | None = None,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> FunctionPrompt:
"""Create a Prompt from a function.
@ -377,7 +377,7 @@ def prompt(
tags: set[str] | None = None,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> Callable[[F], F]: ...
@overload
def prompt(
@ -391,7 +391,7 @@ def prompt(
tags: set[str] | None = None,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> Callable[[F], F]: ...
@ -406,7 +406,7 @@ def prompt(
tags: set[str] | None = None,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> Any:
"""Standalone decorator to mark a function as an MCP prompt.

View file

@ -27,8 +27,8 @@ from mcp.types import PromptArgument as SDKPromptArgument
from pydantic import Field
from pydantic.json_schema import SkipJsonSchema
from fastmcp.server.auth.authorization import AuthCheck
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
from fastmcp.tools.tool import AuthCheckCallable
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import (
@ -195,7 +195,7 @@ class Prompt(FastMCPComponent):
arguments: list[PromptArgument] | None = Field(
default=None, description="Arguments that can be passed to the prompt"
)
auth: SkipJsonSchema[AuthCheckCallable | list[AuthCheckCallable] | None] = Field(
auth: SkipJsonSchema[AuthCheck | list[AuthCheck] | None] = Field(
default=None, description="Authorization checks for this prompt", exclude=True
)
@ -237,7 +237,7 @@ class Prompt(FastMCPComponent):
tags: set[str] | None = None,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> FunctionPrompt:
"""Create a Prompt from a function.

View file

@ -16,12 +16,12 @@ import fastmcp
from fastmcp.decorators import resolve_task_config
from fastmcp.resources.resource import Resource, ResourceResult
from fastmcp.server.apps import resolve_ui_mime_type
from fastmcp.server.auth.authorization import AuthCheck
from fastmcp.server.dependencies import (
transform_context_annotations,
without_injected_parameters,
)
from fastmcp.server.tasks.config import TaskConfig
from fastmcp.tools.tool import AuthCheckCallable
from fastmcp.utilities.async_utils import call_sync_fn_in_threadpool
if TYPE_CHECKING:
@ -57,7 +57,7 @@ class ResourceMeta:
annotations: Annotations | None = None
meta: dict[str, Any] | None = None
task: bool | TaskConfig | None = None
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None
auth: AuthCheck | list[AuthCheck] | None = None
enabled: bool = True
@ -94,7 +94,7 @@ class FunctionResource(Resource):
annotations: Annotations | None = None,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> FunctionResource:
"""Create a FunctionResource from a function.
@ -246,7 +246,7 @@ def resource(
annotations: Annotations | dict[str, Any] | None = None,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> Callable[[F], F]:
"""Standalone decorator to mark a function as an MCP resource.

View file

@ -29,8 +29,8 @@ from pydantic import (
from pydantic.json_schema import SkipJsonSchema
from typing_extensions import Self
from fastmcp.server.auth.authorization import AuthCheck
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
from fastmcp.tools.tool import AuthCheckCallable
from fastmcp.utilities.components import FastMCPComponent
@ -227,7 +227,7 @@ class Resource(FastMCPComponent):
Field(description="Optional annotations about the resource's behavior"),
] = None
auth: Annotated[
SkipJsonSchema[AuthCheckCallable | list[AuthCheckCallable] | None],
SkipJsonSchema[AuthCheck | list[AuthCheck] | None],
Field(description="Authorization checks for this resource", exclude=True),
] = None
@ -247,7 +247,7 @@ class Resource(FastMCPComponent):
annotations: Annotations | None = None,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> FunctionResource:
from fastmcp.resources.function_resource import (
FunctionResource,

View file

@ -24,12 +24,12 @@ from pydantic import (
from fastmcp.resources.resource import Resource, ResourceResult
from fastmcp.server.apps import resolve_ui_mime_type
from fastmcp.server.auth.authorization import AuthCheck
from fastmcp.server.dependencies import (
transform_context_annotations,
without_injected_parameters,
)
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
from fastmcp.tools.tool import AuthCheckCallable
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.json_schema import compress_schema
from fastmcp.utilities.types import get_cached_typeadapter
@ -117,7 +117,7 @@ class ResourceTemplate(FastMCPComponent):
annotations: Annotations | None = Field(
default=None, description="Optional annotations about the resource's behavior"
)
auth: SkipJsonSchema[AuthCheckCallable | list[AuthCheckCallable] | None] = Field(
auth: SkipJsonSchema[AuthCheck | list[AuthCheck] | None] = Field(
default=None,
description="Authorization checks for this resource template",
exclude=True,
@ -140,7 +140,7 @@ class ResourceTemplate(FastMCPComponent):
annotations: Annotations | None = None,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> FunctionResourceTemplate:
return FunctionResourceTemplate.from_function(
fn=fn,
@ -471,7 +471,7 @@ class FunctionResourceTemplate(ResourceTemplate):
annotations: Annotations | None = None,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> FunctionResourceTemplate:
"""Create a template from a function."""

View file

@ -28,8 +28,9 @@ Example:
from __future__ import annotations
import inspect
import logging
from collections.abc import Callable
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, cast
@ -70,8 +71,8 @@ class AuthContext:
return self.component if isinstance(self.component, Tool) else None
# Type alias for auth check functions
AuthCheck = Callable[[AuthContext], bool]
# Type alias for auth check functions (sync or async)
AuthCheck = Callable[[AuthContext], bool] | Callable[[AuthContext], Awaitable[bool]]
def require_scopes(*scopes: str) -> AuthCheck:
@ -130,13 +131,14 @@ def restrict_tag(tag: str, *, scopes: list[str]) -> AuthCheck:
return check
def run_auth_checks(
async def run_auth_checks(
checks: AuthCheck | list[AuthCheck],
ctx: AuthContext,
) -> bool:
"""Run auth checks with AND logic.
All checks must pass for authorization to succeed.
All checks must pass for authorization to succeed. Checks can be
synchronous or asynchronous functions.
Auth checks can:
- Return True to allow access
@ -146,6 +148,7 @@ def run_auth_checks(
Args:
checks: A single check function or list of check functions.
Each check can be sync (returns bool) or async (returns Awaitable[bool]).
ctx: The auth context to pass to each check.
Returns:
@ -159,7 +162,10 @@ def run_auth_checks(
for check in check_list:
try:
if not check(ctx):
result = check(ctx)
if inspect.isawaitable(result):
result = await result
if not result:
return False
except AuthorizationError:
# Let AuthorizationError propagate with its custom message

View file

@ -6,7 +6,7 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast
from key_value.aio.protocols import AsyncKeyValue
@ -16,6 +16,7 @@ from fastmcp.utilities.auth import decode_jwt_payload, parse_scopes
from fastmcp.utilities.logging import get_logger
if TYPE_CHECKING:
from azure.identity.aio import OnBehalfOfCredential
from mcp.server.auth.provider import AuthorizationParams
from mcp.shared.auth import OAuthClientInformationFull
@ -161,6 +162,10 @@ class AzureProvider(OAuthProxy):
if "offline_access" not in parsed_additional_scopes:
parsed_additional_scopes = [*parsed_additional_scopes, "offline_access"]
# Store Azure-specific config for OBO credential creation
self._tenant_id = tenant_id
self._base_authority = base_authority
# Apply defaults
self.identifier_uri = identifier_uri or f"api://{client_id}"
self.additional_authorize_scopes: list[str] = parsed_additional_scopes
@ -453,6 +458,33 @@ class AzureProvider(OAuthProxy):
logger.debug("Failed to extract Azure claims: %s", e)
return None
def create_obo_credential(self, user_assertion: str) -> OnBehalfOfCredential:
"""Create an OnBehalfOfCredential for OBO token exchange.
Uses the AzureProvider's configuration (client_id, client_secret,
tenant_id, authority) to create a credential that can exchange the
user's token for downstream API tokens.
Args:
user_assertion: The user's access token to exchange via OBO.
Returns:
A configured OnBehalfOfCredential ready for get_token() calls.
Raises:
ImportError: If azure-identity is not installed (requires fastmcp[azure]).
"""
_require_azure_identity("OBO token exchange")
from azure.identity.aio import OnBehalfOfCredential
return OnBehalfOfCredential(
tenant_id=self._tenant_id,
client_id=self._upstream_client_id,
client_secret=self._upstream_client_secret.get_secret_value(),
user_assertion=user_assertion,
authority=f"https://{self._base_authority}",
)
class AzureJWTVerifier(JWTVerifier):
"""JWT verifier pre-configured for Azure AD / Microsoft Entra ID.
@ -552,3 +584,117 @@ class AzureJWTVerifier(JWTVerifier):
else:
prefixed.append(f"{self._identifier_uri}/{scope}")
return prefixed
# --- Dependency injection support ---
# These require fastmcp[azure] extra for azure-identity
# Check if DI engine is available
try:
from docket.dependencies import Dependency
except ImportError:
from fastmcp._vendor.docket_di import Dependency
def _require_azure_identity(feature: str) -> None:
"""Raise ImportError with install instructions if azure-identity is not available."""
try:
import azure.identity # noqa: F401
except ImportError as e:
raise ImportError(
f"{feature} requires the `azure` extra. "
"Install with: pip install 'fastmcp[azure]'"
) from e
class _EntraOBOToken(Dependency): # type: ignore[misc]
"""Dependency that performs OBO token exchange for Microsoft Entra.
Uses azure.identity's OnBehalfOfCredential for async-native OBO,
with automatic token caching and refresh.
"""
def __init__(self, scopes: list[str]):
self.scopes = scopes
self._credential: OnBehalfOfCredential | None = None
async def __aenter__(self) -> str:
_require_azure_identity("EntraOBOToken")
from fastmcp.server.dependencies import get_access_token, get_server
access_token = get_access_token()
if access_token is None:
raise RuntimeError(
"No access token available. Cannot perform OBO exchange."
)
server = get_server()
if not isinstance(server.auth, AzureProvider):
raise RuntimeError(
"EntraOBOToken requires an AzureProvider as the auth provider. "
f"Current provider: {type(server.auth).__name__}"
)
self._credential = server.auth.create_obo_credential(
user_assertion=access_token.token,
)
try:
result = await self._credential.get_token(*self.scopes)
except BaseException:
await self._credential.close()
self._credential = None
raise
return result.token
async def __aexit__(self, *args: object) -> None:
if self._credential is not None:
await self._credential.close()
self._credential = None
def EntraOBOToken(scopes: list[str]) -> str:
"""Exchange the user's Entra token for a downstream API token via OBO.
This dependency performs a Microsoft Entra On-Behalf-Of (OBO) token exchange,
allowing your MCP server to call downstream APIs (like Microsoft Graph) on
behalf of the authenticated user.
Args:
scopes: The scopes to request for the downstream API. For Microsoft Graph,
use scopes like ["https://graph.microsoft.com/Mail.Read"] or
["https://graph.microsoft.com/.default"].
Returns:
A dependency that resolves to the downstream API access token string
Raises:
ImportError: If fastmcp[azure] is not installed
RuntimeError: If no access token is available, provider is not Azure,
or OBO exchange fails
Example:
```python
from fastmcp.server.auth.providers.azure import EntraOBOToken
import httpx
@mcp.tool()
async def get_my_emails(
graph_token: str = EntraOBOToken(["https://graph.microsoft.com/Mail.Read"])
):
async with httpx.AsyncClient() as client:
resp = await client.get(
"https://graph.microsoft.com/v1.0/me/messages",
headers={"Authorization": f"Bearer {graph_token}"}
)
return resp.json()
```
Note:
For OBO to work, ensure the scopes are included in the AzureProvider's
`additional_authorize_scopes` parameter, and that admin consent has been
granted for those scopes in your Entra app registration.
"""
return cast(str, _EntraOBOToken(scopes))

View file

@ -161,6 +161,9 @@ class Context:
await ctx.set_state("key", "value")
value = await ctx.get_state("key")
# Store non-serializable values for the current request only
await ctx.set_state("client", http_client, serializable=False)
return str(x)
```
@ -194,6 +197,8 @@ class Context:
self._tokens: list[Token] = []
# Background task support (SEP-1686)
self._task_id: str | None = task_id
# Request-scoped state for non-serializable values (serializable=False)
self._request_state: dict[str, Any] = {}
@property
def is_background_task(self) -> bool:
@ -318,6 +323,10 @@ class Context:
Returns an empty dict if no lifespan was configured or if the MCP
session is not yet established.
In background tasks (Docket workers), where request_context is not
available, falls back to reading from the FastMCP server's lifespan
result directly.
Example:
```python
@server.tool
@ -330,6 +339,11 @@ class Context:
"""
rc = self.request_context
if rc is None:
# In background tasks, request_context is not available.
# Fall back to the server's lifespan result directly (#3095).
result = self.fastmcp._lifespan_result
if result is not None:
return result
return {}
return rc.lifespan_context
@ -338,9 +352,13 @@ class Context:
) -> None:
"""Report progress for the current operation.
Works in both foreground (MCP progress notifications) and background
(Docket task execution) contexts.
Args:
progress: Current progress value e.g. 24
total: Optional total value e.g. 100
message: Optional status message describing current progress
"""
progress_token = (
@ -349,16 +367,48 @@ class Context:
else None
)
if progress_token is None:
# Foreground: Send MCP progress notification if we have a token
if progress_token is not None:
await self.session.send_progress_notification(
progress_token=progress_token,
progress=progress,
total=total,
message=message,
related_request_id=self.request_id,
)
return
await self.session.send_progress_notification(
progress_token=progress_token,
progress=progress,
total=total,
message=message,
related_request_id=self.request_id,
)
# Background: Update Docket execution progress (stored in Redis)
# This makes progress visible via tasks/get and notifications/tasks/status
from fastmcp.server.dependencies import is_docket_available
if not is_docket_available():
return
try:
from docket.dependencies import Dependency
# Get current execution from worker context
execution = Dependency.execution.get()
# Update progress in Redis using Docket's progress API.
# Docket only exposes increment() (relative), so we compute
# the delta from the last reported value stored on this execution.
if total is not None:
await execution.progress.set_total(int(total))
current = int(progress)
last: int = getattr(execution, "_fastmcp_last_progress", 0)
delta = current - last
if delta > 0:
await execution.progress.increment(delta)
execution._fastmcp_last_progress = current # type: ignore[attr-defined]
if message is not None:
await execution.progress.set_message(message)
except LookupError:
# Not running in Docket worker context - no progress tracking available
pass
async def _paginate_list(
self,
@ -378,12 +428,16 @@ class Context:
"""
all_items: list[Any] = []
cursor: str | None = None
seen_cursors: set[str] = set()
while True:
request = request_factory(cursor)
result = await call_method(request)
all_items.extend(extract_items(result))
if result.nextCursor is None:
if not result.nextCursor:
break
if result.nextCursor in seen_cursors:
break
seen_cursors.add(result.nextCursor)
cursor = result.nextCursor
return all_items
@ -754,6 +808,7 @@ class Context:
tool_choice: ToolChoiceOption | str | None = None,
execute_tools: bool = True,
mask_error_details: bool | None = None,
tool_concurrency: int | None = None,
) -> SampleStep:
"""
Make a single LLM sampling call.
@ -777,6 +832,12 @@ class Context:
mask_error_details: If True, mask detailed error messages from tool
execution. When None (default), uses the global settings value.
Tools can raise ToolError to bypass masking.
tool_concurrency: Controls parallel execution of tools:
- None (default): Sequential execution (one at a time)
- 0: Unlimited parallel execution
- N > 0: Execute at most N tools concurrently
If any tool has sequential=True, all tools execute sequentially
regardless of this setting.
Returns:
SampleStep containing:
@ -810,6 +871,7 @@ class Context:
tool_choice=tool_choice,
auto_execute_tools=execute_tools,
mask_error_details=mask_error_details,
tool_concurrency=tool_concurrency,
)
@overload
@ -824,6 +886,7 @@ class Context:
tools: Sequence[SamplingTool | Callable[..., Any]] | None = None,
result_type: type[ResultT],
mask_error_details: bool | None = None,
tool_concurrency: int | None = None,
) -> SamplingResult[ResultT]:
"""Overload: With result_type, returns SamplingResult[ResultT]."""
@ -839,6 +902,7 @@ class Context:
tools: Sequence[SamplingTool | Callable[..., Any]] | None = None,
result_type: None = None,
mask_error_details: bool | None = None,
tool_concurrency: int | None = None,
) -> SamplingResult[str]:
"""Overload: Without result_type, returns SamplingResult[str]."""
@ -853,6 +917,7 @@ class Context:
tools: Sequence[SamplingTool | Callable[..., Any]] | None = None,
result_type: type[ResultT] | None = None,
mask_error_details: bool | None = None,
tool_concurrency: int | None = None,
) -> SamplingResult[ResultT] | SamplingResult[str]:
"""
Send a sampling request to the client and await the response.
@ -883,6 +948,12 @@ class Context:
mask_error_details: If True, mask detailed error messages from tool
execution. When None (default), uses the global settings value.
Tools can raise ToolError to bypass masking.
tool_concurrency: Controls parallel execution of tools:
- None (default): Sequential execution (one at a time)
- 0: Unlimited parallel execution
- N > 0: Execute at most N tools concurrently
If any tool has sequential=True, all tools execute sequentially
regardless of this setting.
Returns:
SamplingResult[T] containing:
@ -906,6 +977,7 @@ class Context:
tools=tools,
result_type=result_type,
mask_error_details=mask_error_details,
tool_concurrency=tool_concurrency,
)
@overload
@ -1079,7 +1151,7 @@ class Context:
return await elicit_for_task(
task_id=self._task_id, # type: ignore[arg-type]
session=self.session,
session=self._session,
message=message,
schema=schema,
fastmcp=self.fastmcp,
@ -1089,32 +1161,69 @@ class Context:
"""Create session-prefixed key for state storage."""
return f"{self.session_id}:{key}"
async def set_state(self, key: str, value: Any) -> None:
"""Set a value in the session-scoped state store.
async def set_state(
self, key: str, value: Any, *, serializable: bool = True
) -> None:
"""Set a value in the state store.
By default, values are stored in the session-scoped state store and
persist across requests within the same MCP session. Values must be
JSON-serializable (dicts, lists, strings, numbers, etc.).
For non-serializable values (e.g., HTTP clients, database connections),
pass ``serializable=False``. These values are stored in a request-scoped
dict and only live for the current MCP request (tool call, resource
read, or prompt render). They will not be available in subsequent
requests.
Values persist across requests within the same MCP session.
The key is automatically prefixed with the session identifier.
State expires after 1 day to prevent unbounded memory growth.
"""
prefixed_key = self._make_state_key(key)
await self.fastmcp._state_store.put(
key=prefixed_key,
value=StateValue(value=value),
ttl=self._STATE_TTL_SECONDS,
)
if not serializable:
self._request_state[prefixed_key] = value
return
# Clear any request-scoped shadow so the session value is visible
self._request_state.pop(prefixed_key, None)
try:
await self.fastmcp._state_store.put(
key=prefixed_key,
value=StateValue(value=value),
ttl=self._STATE_TTL_SECONDS,
)
except Exception as e:
# Catch serialization errors from Pydantic (ValueError) or
# the key_value library (SerializationError). Both contain
# "serialize" in the message. Other exceptions propagate as-is.
if "serialize" in str(e).lower():
raise TypeError(
f"Value for state key {key!r} is not serializable. "
f"Use set_state({key!r}, value, serializable=False) to store "
f"non-serializable values. Note: non-serializable state is "
f"request-scoped and will not persist across requests."
) from e
raise
async def get_state(self, key: str) -> Any:
"""Get a value from the session-scoped state store.
"""Get a value from the state store.
Checks request-scoped state first (set with ``serializable=False``),
then falls back to the session-scoped state store.
Returns None if the key is not found.
"""
prefixed_key = self._make_state_key(key)
if prefixed_key in self._request_state:
return self._request_state[prefixed_key]
result = await self.fastmcp._state_store.get(key=prefixed_key)
return result.value if result is not None else None
async def delete_state(self, key: str) -> None:
"""Delete a value from the session-scoped state store."""
"""Delete a value from the state store.
Removes from both request-scoped and session-scoped stores.
"""
prefixed_key = self._make_state_key(key)
self._request_state.pop(prefixed_key, None)
await self.fastmcp._state_store.delete(key=prefixed_key)
# -------------------------------------------------------------------------

View file

@ -9,11 +9,13 @@ from __future__ import annotations
import contextlib
import inspect
import logging
import weakref
from collections.abc import AsyncGenerator, Callable
from contextlib import AsyncExitStack, asynccontextmanager
from contextvars import ContextVar
from contextvars import ContextVar, Token
from dataclasses import dataclass
from datetime import datetime, timezone
from functools import lru_cache
from typing import TYPE_CHECKING, Any, Protocol, cast, get_type_hints, runtime_checkable
@ -33,6 +35,8 @@ from fastmcp.server.http import _current_http_request
from fastmcp.utilities.async_utils import call_sync_fn_in_threadpool
from fastmcp.utilities.types import find_kwarg_by_type, is_class_member_of_type
_logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from docket import Docket
from docket.worker import Worker
@ -53,6 +57,7 @@ __all__ = [
"CurrentWorker",
"Progress",
"TaskContextInfo",
"TokenClaim",
"get_access_token",
"get_context",
"get_http_headers",
@ -165,6 +170,9 @@ _current_server: ContextVar[weakref.ref[FastMCP] | None] = ContextVar(
)
_current_docket: ContextVar[Docket | None] = ContextVar("docket", default=None)
_current_worker: ContextVar[Worker | None] = ContextVar("worker", default=None)
_task_access_token: ContextVar[AccessToken | None] = ContextVar(
"task_access_token", default=None
)
# --- Docket availability check ---
@ -478,7 +486,8 @@ def get_access_token() -> AccessToken | None:
This function first tries to get the token from the current HTTP request's scope,
which is more reliable for long-lived connections where the SDK's auth_context_var
may become stale after token refresh. Falls back to the SDK's context var if no
request is available.
request is available. In background tasks (Docket workers), falls back to the
token snapshot stored in Redis at task submission time.
Returns:
The access token if an authenticated user is available, None otherwise.
@ -501,6 +510,19 @@ def get_access_token() -> AccessToken | None:
if access_token is None:
access_token = _sdk_get_access_token()
# Fall back to background task snapshot (#3095)
# In Docket workers, neither HTTP request nor SDK context var are available.
# The token was snapshotted in Redis at submit_to_docket() time and restored
# into this ContextVar by _CurrentContext.__aenter__().
if access_token is None:
task_token = _task_access_token.get()
if task_token is not None:
# Check expiration: if expires_at is set and past, treat as expired
if task_token.expires_at is not None:
if task_token.expires_at < int(datetime.now(timezone.utc).timestamp()):
return None
return task_token
if access_token is None or isinstance(access_token, AccessToken):
return access_token
@ -718,14 +740,49 @@ async def resolve_dependencies(
# so that get_dependency_parameters can detect them.
async def _restore_task_access_token(
session_id: str, task_id: str
) -> Token[AccessToken | None] | None:
"""Restore the access token snapshot from Redis into a ContextVar.
Called when setting up context in a Docket worker. The token was stored at
submit_to_docket() time. The token is restored regardless of expiration;
get_access_token() checks expiry when reading from the ContextVar.
Returns:
The ContextVar token for resetting, or None if nothing was restored.
"""
docket = _current_docket.get()
if docket is None:
return None
token_key = docket.key(f"fastmcp:task:{session_id}:{task_id}:access_token")
try:
async with docket.redis() as redis:
token_data = await redis.get(token_key)
if token_data is not None:
restored = AccessToken.model_validate_json(token_data)
return _task_access_token.set(restored)
except Exception:
_logger.warning(
"Failed to restore access token for task %s:%s",
session_id,
task_id,
exc_info=True,
)
return None
class _CurrentContext(Dependency): # type: ignore[misc]
"""Async context manager for Context dependency.
In foreground (request) mode: returns the active context from _current_context.
In background (Docket worker) mode: creates a task-aware Context with task_id.
In background (Docket worker) mode: creates a task-aware Context with task_id
and restores the access token snapshot from Redis.
"""
_context: Context | None = None
_access_token_cv_token: Token[AccessToken | None] | None = None
async def __aenter__(self) -> Context:
from fastmcp.server.context import Context, _current_context
@ -750,6 +807,12 @@ class _CurrentContext(Dependency): # type: ignore[misc]
)
# Enter the context to set up ContextVars
await self._context.__aenter__()
# Restore access token snapshot from Redis (#3095)
self._access_token_cv_token = await _restore_task_access_token(
task_info.session_id, task_info.task_id
)
return self._context
# Neither foreground nor background context available
@ -761,6 +824,10 @@ class _CurrentContext(Dependency): # type: ignore[misc]
)
async def __aexit__(self, *args: object) -> None:
# Clean up access token ContextVar
if self._access_token_cv_token is not None:
_task_access_token.reset(self._access_token_cv_token)
self._access_token_cv_token = None
# Clean up if we created a context for background task
if self._context is not None:
await self._context.__aexit__(*args)
@ -991,47 +1058,6 @@ def CurrentHeaders() -> dict[str, str]:
return cast(dict[str, str], _CurrentHeaders())
class _CurrentAccessToken(Dependency): # type: ignore[misc]
"""Async context manager for AccessToken dependency."""
async def __aenter__(self) -> AccessToken:
token = get_access_token()
if token is None:
raise RuntimeError(
"No access token found. Ensure authentication is configured "
"and the request is authenticated."
)
return token
async def __aexit__(self, *args: object) -> None:
pass
def CurrentAccessToken() -> AccessToken:
"""Get the current access token for the authenticated user.
This dependency provides access to the AccessToken for the current
authenticated request. Raises an error if no authentication is present.
Returns:
A dependency that resolves to the active AccessToken
Raises:
RuntimeError: If no authenticated user (use get_access_token() for optional)
Example:
```python
from fastmcp.server.dependencies import CurrentAccessToken
from fastmcp.server.auth import AccessToken
@mcp.tool()
async def get_user_id(token: AccessToken = CurrentAccessToken()) -> str:
return token.claims.get("sub", "unknown")
```
"""
return cast(AccessToken, _CurrentAccessToken())
# --- Progress dependency ---
@ -1162,3 +1188,122 @@ class Progress(Dependency): # type: ignore[misc]
async def __aexit__(self, *args: object) -> None:
pass
# --- Access Token dependency ---
class _CurrentAccessToken(Dependency): # type: ignore[misc]
"""Async context manager for AccessToken dependency."""
_access_token_cv_token: Token[AccessToken | None] | None = None
async def __aenter__(self) -> AccessToken:
token = get_access_token()
# If no token found and we're in a Docket worker, try restoring from
# Redis. This handles the case where ctx: Context is not in the
# function signature, so _CurrentContext never ran the restoration.
if token is None:
task_info = get_task_context()
if task_info is not None:
self._access_token_cv_token = await _restore_task_access_token(
task_info.session_id, task_info.task_id
)
token = get_access_token()
if token is None:
raise RuntimeError(
"No access token found. Ensure authentication is configured "
"and the request is authenticated."
)
return token
async def __aexit__(self, *args: object) -> None:
if self._access_token_cv_token is not None:
_task_access_token.reset(self._access_token_cv_token)
self._access_token_cv_token = None
def CurrentAccessToken() -> AccessToken:
"""Get the current access token for the authenticated user.
This dependency provides access to the AccessToken for the current
authenticated request. Raises an error if no authentication is present.
Returns:
A dependency that resolves to the active AccessToken
Raises:
RuntimeError: If no authenticated user (use get_access_token() for optional)
Example:
```python
from fastmcp.server.dependencies import CurrentAccessToken
from fastmcp.server.auth import AccessToken
@mcp.tool()
async def get_user_id(token: AccessToken = CurrentAccessToken()) -> str:
return token.claims.get("sub", "unknown")
```
"""
return cast(AccessToken, _CurrentAccessToken())
# --- Token Claim dependency ---
class _TokenClaim(Dependency): # type: ignore[misc]
"""Dependency that extracts a specific claim from the access token."""
def __init__(self, claim_name: str):
self.claim_name = claim_name
async def __aenter__(self) -> str:
token = get_access_token()
if token is None:
raise RuntimeError(
f"No access token available. Cannot extract claim '{self.claim_name}'."
)
value = token.claims.get(self.claim_name)
if value is None:
raise RuntimeError(
f"Claim '{self.claim_name}' not found in access token. "
f"Available claims: {list(token.claims.keys())}"
)
return str(value)
async def __aexit__(self, *args: object) -> None:
pass
def TokenClaim(name: str) -> str:
"""Get a specific claim from the access token.
This dependency extracts a single claim value from the current access token.
It's useful for getting user identifiers, roles, or other token claims
without needing the full token object.
Args:
name: The name of the claim to extract (e.g., "oid", "sub", "email")
Returns:
A dependency that resolves to the claim value as a string
Raises:
RuntimeError: If no access token is available or claim is missing
Example:
```python
from fastmcp.server.dependencies import TokenClaim
@mcp.tool()
async def add_expense(
user_id: str = TokenClaim("oid"), # Azure object ID
amount: float,
):
# user_id is automatically injected from the token
await db.insert({"user_id": user_id, "amount": amount})
```
"""
return cast(str, _TokenClaim(name))

View file

@ -102,7 +102,7 @@ class AuthMiddleware(Middleware):
authorized_tools: list[Tool] = []
for tool in tools:
ctx = AuthContext(token=token, component=tool)
if run_auth_checks(self.auth, ctx):
if await run_auth_checks(self.auth, ctx):
authorized_tools.append(tool)
return authorized_tools
@ -143,7 +143,7 @@ class AuthMiddleware(Middleware):
# Global auth check
token = get_access_token()
ctx = AuthContext(token=token, component=tool)
if not run_auth_checks(self.auth, ctx):
if not await run_auth_checks(self.auth, ctx):
raise AuthorizationError(
f"Authorization failed for tool '{tool_name}': insufficient permissions"
)
@ -169,7 +169,7 @@ class AuthMiddleware(Middleware):
authorized_resources: list[Resource] = []
for resource in resources:
ctx = AuthContext(token=token, component=resource)
if run_auth_checks(self.auth, ctx):
if await run_auth_checks(self.auth, ctx):
authorized_resources.append(resource)
return authorized_resources
@ -210,7 +210,7 @@ class AuthMiddleware(Middleware):
# Global auth check
token = get_access_token()
ctx = AuthContext(token=token, component=component)
if not run_auth_checks(self.auth, ctx):
if not await run_auth_checks(self.auth, ctx):
raise AuthorizationError(
f"Authorization failed for resource '{uri}': insufficient permissions"
)
@ -238,7 +238,7 @@ class AuthMiddleware(Middleware):
authorized_templates: list[ResourceTemplate] = []
for template in templates:
ctx = AuthContext(token=token, component=template)
if run_auth_checks(self.auth, ctx):
if await run_auth_checks(self.auth, ctx):
authorized_templates.append(template)
return authorized_templates
@ -262,7 +262,7 @@ class AuthMiddleware(Middleware):
authorized_prompts: list[Prompt] = []
for prompt in prompts:
ctx = AuthContext(token=token, component=prompt)
if run_auth_checks(self.auth, ctx):
if await run_auth_checks(self.auth, ctx):
authorized_prompts.append(prompt)
return authorized_prompts
@ -301,7 +301,7 @@ class AuthMiddleware(Middleware):
# Global auth check
token = get_access_token()
ctx = AuthContext(token=token, component=prompt)
if not run_auth_checks(self.auth, ctx):
if not await run_auth_checks(self.auth, ctx):
raise AuthorizationError(
f"Authorization failed for prompt '{prompt_name}': insufficient permissions"
)

View file

@ -243,43 +243,41 @@ class ResponseCachingMiddleware(Middleware):
call_tool_settings or CallToolSettings()
)
# PydanticAdapter type signature will be fixed to accept generic aliases
# See: https://github.com/strawgate/py-key-value/pull/250
self._list_tools_cache: PydanticAdapter[list[Tool]] = PydanticAdapter(
key_value=self._stats,
pydantic_model=list[Tool], # type: ignore[arg-type]
pydantic_model=list[Tool],
default_collection="tools/list",
)
self._list_resources_cache: PydanticAdapter[list[Resource]] = PydanticAdapter(
key_value=self._stats,
pydantic_model=list[Resource], # type: ignore[arg-type]
pydantic_model=list[Resource],
default_collection="resources/list",
)
self._list_prompts_cache: PydanticAdapter[list[Prompt]] = PydanticAdapter(
key_value=self._stats,
pydantic_model=list[Prompt], # type: ignore[arg-type]
pydantic_model=list[Prompt],
default_collection="prompts/list",
)
self._read_resource_cache: PydanticAdapter[CachableResourceResult] = (
PydanticAdapter(
key_value=self._stats,
pydantic_model=CachableResourceResult, # type: ignore[arg-type]
pydantic_model=CachableResourceResult,
default_collection="resources/read",
)
)
self._get_prompt_cache: PydanticAdapter[CachablePromptResult] = PydanticAdapter(
key_value=self._stats,
pydantic_model=CachablePromptResult, # type: ignore[arg-type]
pydantic_model=CachablePromptResult,
default_collection="prompts/get",
)
self._call_tool_cache: PydanticAdapter[CachableToolResult] = PydanticAdapter(
key_value=self._stats,
pydantic_model=CachableToolResult, # type: ignore[arg-type]
pydantic_model=CachableToolResult,
default_collection="tools/call",
)

View file

@ -0,0 +1,78 @@
"""Middleware that dereferences $ref in JSON schemas before sending to clients."""
from collections.abc import Sequence
from typing import Any
import mcp.types as mt
from typing_extensions import override
from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.middleware.middleware import CallNext, Middleware, MiddlewareContext
from fastmcp.tools.tool import Tool
from fastmcp.utilities.json_schema import dereference_refs
class DereferenceRefsMiddleware(Middleware):
"""Dereferences $ref in component schemas before sending to clients.
Some MCP clients (e.g., VS Code Copilot) don't handle JSON Schema $ref
properly. This middleware inlines all $ref definitions so schemas are
self-contained. Enabled by default via ``FastMCP(dereference_schemas=True)``.
"""
@override
async def on_list_tools(
self,
context: MiddlewareContext[mt.ListToolsRequest],
call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]],
) -> Sequence[Tool]:
tools = await call_next(context)
return [_dereference_tool(tool) for tool in tools]
@override
async def on_list_resource_templates(
self,
context: MiddlewareContext[mt.ListResourceTemplatesRequest],
call_next: CallNext[
mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]
],
) -> Sequence[ResourceTemplate]:
templates = await call_next(context)
return [_dereference_resource_template(t) for t in templates]
def _dereference_tool(tool: Tool) -> Tool:
"""Return a copy of the tool with dereferenced schemas."""
updates: dict[str, object] = {}
if "$defs" in tool.parameters or _has_ref(tool.parameters):
updates["parameters"] = dereference_refs(tool.parameters)
if tool.output_schema is not None and (
"$defs" in tool.output_schema or _has_ref(tool.output_schema)
):
updates["output_schema"] = dereference_refs(tool.output_schema)
if updates:
return tool.model_copy(update=updates)
return tool
def _dereference_resource_template(template: ResourceTemplate) -> ResourceTemplate:
"""Return a copy of the template with dereferenced schemas."""
if "$defs" in template.parameters or _has_ref(template.parameters):
return template.model_copy(
update={"parameters": dereference_refs(template.parameters)}
)
return template
def _has_ref(schema: dict[str, Any]) -> bool:
"""Check if a schema contains any $ref."""
if "$ref" in schema:
return True
for value in schema.values():
if isinstance(value, dict) and _has_ref(value):
return True
if isinstance(value, list):
for item in value:
if isinstance(item, dict) and _has_ref(item):
return True
return False

View file

@ -231,17 +231,15 @@ class TransportMixin:
# Resolve from settings/env var if not explicitly set
if stateless_http is None:
stateless_http = self._deprecated_settings.stateless_http
stateless_http = fastmcp.settings.stateless_http
# SSE doesn't support stateless mode
if stateless_http and transport == "sse":
raise ValueError("SSE transport does not support stateless mode")
host = host or self._deprecated_settings.host
port = port or self._deprecated_settings.port
default_log_level_to_use = (
log_level or self._deprecated_settings.log_level
).lower()
host = host or fastmcp.settings.host
port = port or fastmcp.settings.port
default_log_level_to_use = (log_level or fastmcp.settings.log_level).lower()
app = self.http_app(
path=path,
@ -311,31 +309,30 @@ class TransportMixin:
if transport in ("streamable-http", "http"):
return create_streamable_http_app(
server=self,
streamable_http_path=path
or self._deprecated_settings.streamable_http_path,
streamable_http_path=path or fastmcp.settings.streamable_http_path,
event_store=event_store,
retry_interval=retry_interval,
auth=self.auth,
json_response=(
json_response
if json_response is not None
else self._deprecated_settings.json_response
else fastmcp.settings.json_response
),
stateless_http=(
stateless_http
if stateless_http is not None
else self._deprecated_settings.stateless_http
else fastmcp.settings.stateless_http
),
debug=self._deprecated_settings.debug,
debug=fastmcp.settings.debug,
middleware=middleware,
)
elif transport == "sse":
return create_sse_app(
server=self,
message_path=self._deprecated_settings.message_path,
sse_path=path or self._deprecated_settings.sse_path,
message_path=fastmcp.settings.message_path,
sse_path=path or fastmcp.settings.sse_path,
auth=self.auth,
debug=self._deprecated_settings.debug,
debug=fastmcp.settings.debug,
middleware=middleware,
)
else:

View file

@ -17,8 +17,8 @@ from mcp.types import AnyFunction
import fastmcp
from fastmcp.prompts.function_prompt import FunctionPrompt
from fastmcp.prompts.prompt import Prompt
from fastmcp.server.auth.authorization import AuthCheck
from fastmcp.server.tasks.config import TaskConfig
from fastmcp.tools.tool import AuthCheckCallable
if TYPE_CHECKING:
from fastmcp.server.providers.local_provider import LocalProvider
@ -82,7 +82,7 @@ class PromptDecoratorMixin:
enabled: bool = True,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> FunctionPrompt: ...
@overload
@ -99,7 +99,7 @@ class PromptDecoratorMixin:
enabled: bool = True,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> Callable[[AnyFunction], FunctionPrompt]: ...
def prompt(
@ -115,7 +115,7 @@ class PromptDecoratorMixin:
enabled: bool = True,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> (
Callable[[AnyFunction], FunctionPrompt]
| FunctionPrompt

View file

@ -17,8 +17,8 @@ import fastmcp
from fastmcp.resources.function_resource import resource as standalone_resource
from fastmcp.resources.resource import Resource
from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.auth.authorization import AuthCheck
from fastmcp.server.tasks.config import TaskConfig
from fastmcp.tools.tool import AuthCheckCallable
if TYPE_CHECKING:
from fastmcp.server.providers.local_provider import LocalProvider
@ -117,7 +117,7 @@ class ResourceDecoratorMixin:
annotations: Annotations | dict[str, Any] | None = None,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> Callable[[AnyFunction], Resource | ResourceTemplate | AnyFunction]:
"""Decorator to register a function as a resource.

View file

@ -16,14 +16,15 @@ import mcp.types
from mcp.types import AnyFunction, ToolAnnotations
import fastmcp
from fastmcp.server.auth.authorization import AuthCheck
from fastmcp.server.tasks.config import TaskConfig
from fastmcp.tools.function_tool import FunctionTool
from fastmcp.tools.tool import AuthCheckCallable, Tool
from fastmcp.tools.tool import Tool
from fastmcp.utilities.types import NotSet, NotSetT
try:
from prefab_ui import UIResponse as _PrefabUIResponse
from prefab_ui.components.base import Component as _PrefabComponent
from prefab_ui.response import UIResponse as _PrefabUIResponse
_HAS_PREFAB = True
except ImportError:
@ -207,7 +208,7 @@ class ToolDecoratorMixin:
task: bool | TaskConfig | None = None,
serializer: ToolResultSerializerType | None = None, # Deprecated
timeout: float | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> FunctionTool: ...
@overload
@ -229,7 +230,7 @@ class ToolDecoratorMixin:
task: bool | TaskConfig | None = None,
serializer: ToolResultSerializerType | None = None, # Deprecated
timeout: float | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> Callable[[AnyFunction], FunctionTool]: ...
# NOTE: This method mirrors fastmcp.tools.tool() but adds registration,
@ -254,7 +255,7 @@ class ToolDecoratorMixin:
task: bool | TaskConfig | None = None,
serializer: ToolResultSerializerType | None = None, # Deprecated
timeout: float | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> (
Callable[[AnyFunction], FunctionTool]
| FunctionTool

View file

@ -159,23 +159,28 @@ class OpenAPITool(Tool):
async def run(self, arguments: dict[str, Any]) -> ToolResult:
"""Execute the HTTP request using RequestDirector."""
# Build the request — errors here are programming/schema issues,
# not HTTP failures, so we catch them separately.
try:
base_url = str(self._client.base_url) or "http://localhost"
# Build the request using RequestDirector
request = self._director.build(self._route, arguments, base_url)
# Add client headers (lowest precedence)
if self._client.headers:
for key, value in self._client.headers.items():
if key not in request.headers:
request.headers[key] = value
# Add MCP transport headers (highest precedence)
mcp_headers = get_http_headers()
if mcp_headers:
request.headers.update(mcp_headers)
except Exception as e:
raise ValueError(
f"Error building request for {self._route.method.upper()} "
f"{self._route.path}: {type(e).__name__}: {e}"
) from e
# Send the request and process the response.
try:
logger.debug(f"run - sending request; headers: {request.headers}")
response = await self._client.send(request)
@ -196,6 +201,12 @@ class OpenAPITool(Tool):
else:
structured_output = result
# Structured content must be a dict for the MCP protocol.
# Wrap non-dict values that slipped through (e.g. a backend
# returning an array when the schema declared an object).
if not isinstance(structured_output, dict):
structured_output = {"result": structured_output}
return ToolResult(structured_content=structured_output)
except json.JSONDecodeError:
return ToolResult(content=response.text)

View file

@ -34,7 +34,6 @@ from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.openapi import (
HTTPRoute,
extract_output_schema_from_responses,
format_simple_description,
parse_openapi_to_http_routes,
)
from fastmcp.utilities.openapi.director import RequestDirector
@ -79,6 +78,7 @@ class OpenAPIProvider(Provider):
mcp_component_fn: ComponentFn | None = None,
mcp_names: dict[str, str] | None = None,
tags: set[str] | None = None,
validate_output: bool = True,
):
"""Initialize provider by parsing OpenAPI spec and creating components.
@ -93,6 +93,10 @@ class OpenAPIProvider(Provider):
mcp_component_fn: Optional callable for component customization
mcp_names: Optional dictionary mapping operationId to component names
tags: Optional set of tags to add to all components
validate_output: If True (default), tools use the output schema
extracted from the OpenAPI spec for response validation. If
False, a permissive schema is used instead, allowing any
response structure while still returning structured JSON.
"""
super().__init__()
@ -101,6 +105,7 @@ class OpenAPIProvider(Provider):
client = self._create_default_client(openapi_spec)
self._client = client
self._mcp_component_fn = mcp_component_fn
self._validate_output = validate_output
# Keep track of names to detect collisions
self._used_names: dict[str, Counter[str]] = {
@ -232,24 +237,30 @@ class OpenAPIProvider(Provider):
route.openapi_version,
)
if not self._validate_output and output_schema is not None:
# Use a permissive schema that accepts any object, preserving
# the wrap-result flag so non-object responses still get wrapped
permissive: dict[str, Any] = {
"type": "object",
"additionalProperties": True,
}
if output_schema.get("x-fastmcp-wrap-result"):
permissive["x-fastmcp-wrap-result"] = True
output_schema = permissive
tool_name = self._get_unique_name(name, "tool")
base_description = (
route.description
or route.summary
or f"Executes {route.method} {route.path}"
)
enhanced_description = format_simple_description(
base_description=base_description,
parameters=route.parameters,
request_body=route.request_body,
)
tool = OpenAPITool(
client=self._client,
route=route,
director=self._director,
name=tool_name,
description=enhanced_description,
description=base_description,
parameters=combined_schema,
output_schema=output_schema,
tags=set(route.tags or []) | tags,
@ -276,11 +287,6 @@ class OpenAPIProvider(Provider):
base_description = (
route.description or route.summary or f"Represents {route.path}"
)
enhanced_description = format_simple_description(
base_description=base_description,
parameters=route.parameters,
request_body=route.request_body,
)
resource = OpenAPIResource(
client=self._client,
@ -288,7 +294,7 @@ class OpenAPIProvider(Provider):
director=self._director,
uri=resource_uri,
name=resource_name,
description=enhanced_description,
description=base_description,
mime_type=_extract_mime_type_from_route(route),
tags=set(route.tags or []) | tags,
)
@ -321,11 +327,6 @@ class OpenAPIProvider(Provider):
base_description = (
route.description or route.summary or f"Template for {route.path}"
)
enhanced_description = format_simple_description(
base_description=base_description,
parameters=route.parameters,
request_body=route.request_body,
)
template_params_schema = {
"type": "object",
@ -355,7 +356,7 @@ class OpenAPIProvider(Provider):
director=self._director,
uri_template=uri_template_str,
name=template_name,
description=enhanced_description,
description=base_description,
parameters=template_params_schema,
tags=set(route.tags or []) | tags,
mime_type=_extract_mime_type_from_route(route),

View file

@ -16,6 +16,7 @@ from urllib.parse import quote
import mcp.types
from mcp import ServerSession
from mcp.client.session import ClientSession
from mcp.server.lowlevel.server import request_ctx
from mcp.shared.context import LifespanContextT, RequestContext
from mcp.shared.exceptions import McpError
from mcp.types import (
@ -121,6 +122,12 @@ class ProxyTool(Tool):
client = await self._get_client()
async with client:
ctx = context or get_context()
# StatefulProxyClient reuses sessions across requests, so
# its receive-loop task has stale ContextVars from the first
# request. Stash the current RequestContext in the shared
# ref so handlers can restore it before forwarding.
if isinstance(client, StatefulProxyClient):
cast(list[Any], client._proxy_rc_ref)[0] = ctx.request_context
# Build meta dict from request context
meta: dict[str, Any] | None = None
if hasattr(ctx, "request_context"):
@ -781,16 +788,50 @@ async def default_proxy_progress_handler(
await ctx.report_progress(progress, total, message)
def _restore_request_context(
rc_ref: list[Any],
) -> None:
"""Set the ``request_ctx`` ContextVar from a stashed RequestContext.
Called at the start of proxy handler invocations in
``StatefulProxyClient`` to fix stale ContextVars in the receive-loop
task. Only overrides when the ContextVar is genuinely stale (same
session, different request_id) to avoid corrupting the concurrent
case where multiple sessions share the same ref via ``copy.copy``.
"""
rc = rc_ref[0]
if rc is None:
return
try:
current_rc = request_ctx.get()
except LookupError:
request_ctx.set(rc)
return
if current_rc.session is rc.session and current_rc.request_id != rc.request_id:
request_ctx.set(rc)
def _make_restoring_handler(handler: Callable, rc_ref: list[Any]) -> Callable:
"""Wrap a proxy handler to restore request_ctx before delegating.
The wrapper is a plain ``async def`` so it passes
``inspect.isfunction()`` checks in handler registration paths
(e.g., ``create_roots_callback``).
"""
async def wrapper(*args: Any, **kwargs: Any) -> Any:
_restore_request_context(rc_ref)
return await handler(*args, **kwargs)
return wrapper
class ProxyClient(Client[ClientTransportT]):
"""A proxy client that forwards advanced interactions between a remote MCP server and the proxy's connected clients.
Supports forwarding roots, sampling, elicitation, logging, and progress.
"""
# Stored context for handlers when contextvar isn't available
# (e.g., when receive loop was started before any request context)
_proxy_context: Context | None = None
def __init__(
self,
transport: ClientTransportT
@ -826,9 +867,39 @@ class StatefulProxyClient(ProxyClient[ClientTransportT]):
This is useful to proxy a stateful mcp server such as the Playwright MCP server.
Note that it is essential to ensure that the proxy server itself is also stateful.
Because session reuse means the receive-loop task inherits a stale
``request_ctx`` ContextVar snapshot, the default proxy handlers are
replaced with versions that restore the ContextVar before forwarding.
``ProxyTool.run`` stashes the current ``RequestContext`` in
``_proxy_rc_ref`` before each backend call, and the handlers consult
it to detect (and correct) staleness.
"""
# Mutable list shared across copies (Client.new() uses copy.copy,
# which preserves references to mutable containers). ProxyTool.run
# writes [0] before each backend call; handlers read it to detect
# stale ContextVars and restore the correct request_ctx.
#
# We store the concrete RequestContext (not fastmcp's Context) because
# Context properties are themselves ContextVar-dependent and resolve
# in the caller's async context — which is stale in the receive loop.
_proxy_rc_ref: list[Any]
def __init__(self, *args: Any, **kwargs: Any):
# Install context-restoring handler wrappers BEFORE super().__init__
# registers them with the Client's session kwargs.
self._proxy_rc_ref = [None]
for key, default_fn in (
("roots", default_proxy_roots_handler),
("sampling_handler", default_proxy_sampling_handler),
("elicitation_handler", default_proxy_elicitation_handler),
("log_handler", default_proxy_log_handler),
("progress_handler", default_proxy_progress_handler),
):
if key not in kwargs:
kwargs[key] = _make_restoring_handler(default_fn, self._proxy_rc_ref)
super().__init__(*args, **kwargs)
self._caches: dict[ServerSession, Client[ClientTransportT]] = {}

View file

@ -8,6 +8,7 @@ from collections.abc import Callable, Sequence
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Generic, Literal, cast
import anyio
from mcp.types import (
ClientCapabilities,
CreateMessageResult,
@ -31,6 +32,9 @@ from typing_extensions import TypeVar
from fastmcp import settings
from fastmcp.exceptions import ToolError
from fastmcp.server.sampling.sampling_tool import SamplingTool
from fastmcp.tools.function_tool import FunctionTool
from fastmcp.tools.tool_transform import TransformedTool
from fastmcp.utilities.async_utils import gather
from fastmcp.utilities.json_schema import compress_schema
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import get_cached_typeadapter
@ -239,6 +243,7 @@ async def execute_tools(
tool_calls: list[ToolUseContent],
tool_map: dict[str, SamplingTool],
mask_error_details: bool = False,
tool_concurrency: int | None = None,
) -> list[ToolResultContent]:
"""Execute tool calls and return results.
@ -249,66 +254,96 @@ async def execute_tools(
When masked, only generic error messages are returned to the LLM.
Tools can explicitly raise ToolError to bypass masking when they want
to provide specific error messages to the LLM.
tool_concurrency: Controls parallel execution of tools:
- None (default): Sequential execution (one at a time)
- 0: Unlimited parallel execution
- N > 0: Execute at most N tools concurrently
If any tool has sequential=True, all tools execute sequentially
regardless of this setting.
Returns:
List of tool result content blocks.
List of tool result content blocks in the same order as tool_calls.
"""
tool_results: list[ToolResultContent] = []
if tool_concurrency is not None and tool_concurrency < 0:
raise ValueError(
f"tool_concurrency must be None, 0 (unlimited), or a positive integer, "
f"got {tool_concurrency}"
)
for tool_use in tool_calls:
async def _execute_single_tool(tool_use: ToolUseContent) -> ToolResultContent:
"""Execute a single tool and return its result."""
tool = tool_map.get(tool_use.name)
if tool is None:
tool_results.append(
ToolResultContent(
type="tool_result",
toolUseId=tool_use.id,
content=[
TextContent(
type="text",
text=f"Error: Unknown tool '{tool_use.name}'",
)
],
isError=True,
)
return ToolResultContent(
type="tool_result",
toolUseId=tool_use.id,
content=[
TextContent(
type="text",
text=f"Error: Unknown tool '{tool_use.name}'",
)
],
isError=True,
)
else:
try:
result_value = await tool.run(tool_use.input)
tool_results.append(
ToolResultContent(
type="tool_result",
toolUseId=tool_use.id,
content=[TextContent(type="text", text=str(result_value))],
)
)
except ToolError as e:
# ToolError is the escape hatch - always pass message through
logger.exception(f"Error calling sampling tool '{tool_use.name}'")
tool_results.append(
ToolResultContent(
type="tool_result",
toolUseId=tool_use.id,
content=[TextContent(type="text", text=str(e))],
isError=True,
)
)
except Exception as e:
# Generic exceptions - mask based on setting
logger.exception(f"Error calling sampling tool '{tool_use.name}'")
if mask_error_details:
error_text = f"Error executing tool '{tool_use.name}'"
else:
error_text = f"Error executing tool '{tool_use.name}': {e}"
tool_results.append(
ToolResultContent(
type="tool_result",
toolUseId=tool_use.id,
content=[TextContent(type="text", text=error_text)],
isError=True,
)
)
return tool_results
try:
result_value = await tool.run(tool_use.input)
return ToolResultContent(
type="tool_result",
toolUseId=tool_use.id,
content=[TextContent(type="text", text=str(result_value))],
)
except ToolError as e:
# ToolError is the escape hatch - always pass message through
logger.exception(f"Error calling sampling tool '{tool_use.name}'")
return ToolResultContent(
type="tool_result",
toolUseId=tool_use.id,
content=[TextContent(type="text", text=str(e))],
isError=True,
)
except Exception as e:
# Generic exceptions - mask based on setting
logger.exception(f"Error calling sampling tool '{tool_use.name}'")
if mask_error_details:
error_text = f"Error executing tool '{tool_use.name}'"
else:
error_text = f"Error executing tool '{tool_use.name}': {e}"
return ToolResultContent(
type="tool_result",
toolUseId=tool_use.id,
content=[TextContent(type="text", text=error_text)],
isError=True,
)
# Check if any tool requires sequential execution
requires_sequential = any(
tool.sequential
for tool_use in tool_calls
if (tool := tool_map.get(tool_use.name)) is not None
)
# Execute sequentially if required or if concurrency is None (default)
if tool_concurrency is None or requires_sequential:
tool_results: list[ToolResultContent] = []
for tool_use in tool_calls:
result = await _execute_single_tool(tool_use)
tool_results.append(result)
return tool_results
# Execute in parallel
if tool_concurrency == 0:
# Unlimited parallel execution
return await gather(*[_execute_single_tool(tc) for tc in tool_calls])
else:
# Bounded parallel execution with semaphore
semaphore = anyio.Semaphore(tool_concurrency)
async def bounded_execute(tool_use: ToolUseContent) -> ToolResultContent:
async with semaphore:
return await _execute_single_tool(tool_use)
return await gather(*[bounded_execute(tc) for tc in tool_calls])
# --- Helper functions for sampling ---
@ -334,9 +369,22 @@ def prepare_messages(
def prepare_tools(
tools: Sequence[SamplingTool | Callable[..., Any]] | None,
tools: Sequence[SamplingTool | FunctionTool | TransformedTool | Callable[..., Any]]
| None,
) -> list[SamplingTool] | None:
"""Convert tools to SamplingTool objects."""
"""Convert tools to SamplingTool objects.
Accepts SamplingTool instances, FunctionTool instances, TransformedTool instances,
or plain callable functions. FunctionTool and TransformedTool are converted using
from_callable_tool(), while plain functions use from_function().
Args:
tools: Sequence of tools to prepare. Can be SamplingTool, FunctionTool,
TransformedTool, or plain callable functions.
Returns:
List of SamplingTool instances, or None if tools is None.
"""
if tools is None:
return None
@ -344,10 +392,14 @@ def prepare_tools(
for t in tools:
if isinstance(t, SamplingTool):
sampling_tools.append(t)
elif isinstance(t, (FunctionTool, TransformedTool)):
sampling_tools.append(SamplingTool.from_callable_tool(t))
elif callable(t):
sampling_tools.append(SamplingTool.from_function(t))
else:
raise TypeError(f"Expected SamplingTool or callable, got {type(t)}")
raise TypeError(
f"Expected SamplingTool, FunctionTool, TransformedTool, or callable, got {type(t)}"
)
return sampling_tools if sampling_tools else None
@ -408,10 +460,12 @@ async def sample_step_impl(
temperature: float | None = None,
max_tokens: int | None = None,
model_preferences: ModelPreferences | str | list[str] | None = None,
tools: Sequence[SamplingTool | Callable[..., Any]] | None = None,
tools: Sequence[SamplingTool | FunctionTool | TransformedTool | Callable[..., Any]]
| None = None,
tool_choice: ToolChoiceOption | str | None = None,
auto_execute_tools: bool = True,
mask_error_details: bool | None = None,
tool_concurrency: int | None = None,
) -> SampleStep:
"""Implementation of Context.sample_step().
@ -498,7 +552,10 @@ async def sample_step_impl(
else settings.mask_error_details
)
tool_results: list[ToolResultContent] = await execute_tools(
step_tool_calls, tool_map, mask_error_details=effective_mask
step_tool_calls,
tool_map,
mask_error_details=effective_mask,
tool_concurrency=tool_concurrency,
)
if tool_results:
@ -520,9 +577,11 @@ async def sample_impl(
temperature: float | None = None,
max_tokens: int | None = None,
model_preferences: ModelPreferences | str | list[str] | None = None,
tools: Sequence[SamplingTool | Callable[..., Any]] | None = None,
tools: Sequence[SamplingTool | FunctionTool | TransformedTool | Callable[..., Any]]
| None = None,
result_type: type[ResultT] | None = None,
mask_error_details: bool | None = None,
tool_concurrency: int | None = None,
) -> SamplingResult[ResultT]:
"""Implementation of Context.sample().
@ -561,6 +620,7 @@ async def sample_impl(
tools=sampling_tools,
tool_choice=tool_choice,
mask_error_details=mask_error_details,
tool_concurrency=tool_concurrency,
)
# Check for final_response tool call for structured output

View file

@ -6,10 +6,14 @@ import inspect
from collections.abc import Callable
from typing import Any
from mcp.types import TextContent
from mcp.types import Tool as SDKTool
from pydantic import ConfigDict
from fastmcp.tools.function_parsing import ParsedFunction
from fastmcp.tools.function_tool import FunctionTool
from fastmcp.tools.tool import ToolResult
from fastmcp.tools.tool_transform import TransformedTool
from fastmcp.utilities.types import FastMCPBaseModel
@ -40,6 +44,7 @@ class SamplingTool(FastMCPBaseModel):
description: str | None = None
parameters: dict[str, Any]
fn: Callable[..., Any]
sequential: bool = False
model_config = ConfigDict(arbitrary_types_allowed=True)
@ -79,6 +84,7 @@ class SamplingTool(FastMCPBaseModel):
*,
name: str | None = None,
description: str | None = None,
sequential: bool = False,
) -> SamplingTool:
"""Create a SamplingTool from a function.
@ -89,6 +95,10 @@ class SamplingTool(FastMCPBaseModel):
fn: The function to create a tool from.
name: Optional name override. Defaults to the function's name.
description: Optional description override. Defaults to the function's docstring.
sequential: If True, this tool requires sequential execution and prevents
parallel execution of all tools in the batch. Set to True for tools
with shared state, file writes, or other operations that cannot run
concurrently. Defaults to False.
Returns:
A SamplingTool wrapping the function.
@ -106,4 +116,68 @@ class SamplingTool(FastMCPBaseModel):
description=description or parsed.description,
parameters=parsed.input_schema,
fn=parsed.fn,
sequential=sequential,
)
@classmethod
def from_callable_tool(
cls,
tool: FunctionTool | TransformedTool,
*,
name: str | None = None,
description: str | None = None,
) -> SamplingTool:
"""Create a SamplingTool from a FunctionTool or TransformedTool.
Reuses existing server tools in sampling contexts. For TransformedTool,
the tool's .run() method is used to ensure proper argument transformation,
and the ToolResult is automatically unwrapped.
Args:
tool: A FunctionTool or TransformedTool to convert.
name: Optional name override. Defaults to tool.name.
description: Optional description override. Defaults to tool.description.
Raises:
TypeError: If the tool is not a FunctionTool or TransformedTool.
"""
# Validate that the tool is a supported type
if not isinstance(tool, (FunctionTool, TransformedTool)):
raise TypeError(
f"Expected FunctionTool or TransformedTool, got {type(tool).__name__}. "
"Only callable tools can be converted to SamplingTools."
)
# Both FunctionTool and TransformedTool need .run() to ensure proper
# result processing (serializers, output_schema, wrap-result flags)
async def wrapper(**kwargs: Any) -> Any:
result = await tool.run(kwargs)
# Unwrap ToolResult - extract the actual value
if isinstance(result, ToolResult):
# If there's structured_content, use that
if result.structured_content is not None:
# Check tool's schema - this is the source of truth
if tool.output_schema and tool.output_schema.get(
"x-fastmcp-wrap-result"
):
# Tool wraps results: {"result": value} -> value
return result.structured_content.get("result")
else:
# No wrapping: use structured_content directly
return result.structured_content
# Otherwise, extract from text content
if result.content and len(result.content) > 0:
first_content = result.content[0]
if isinstance(first_content, TextContent):
return first_content.text
return result
fn = wrapper
# Extract the callable function, name, description, and parameters
return cls(
name=name or tool.name,
description=description or tool.description,
parameters=tool.parameters,
fn=fn,
)

View file

@ -10,8 +10,6 @@ from collections.abc import (
AsyncIterator,
Awaitable,
Callable,
Collection,
Mapping,
Sequence,
)
from contextlib import (
@ -63,7 +61,7 @@ from fastmcp.server.apps import (
app_config_to_meta_dict,
resolve_ui_mime_type,
)
from fastmcp.server.auth import AuthContext, AuthProvider, run_auth_checks
from fastmcp.server.auth import AuthCheck, AuthContext, AuthProvider, run_auth_checks
from fastmcp.server.dependencies import get_access_token
from fastmcp.server.lifespan import Lifespan
from fastmcp.server.low_level import LowLevelServer
@ -79,9 +77,8 @@ from fastmcp.server.transforms import (
)
from fastmcp.server.transforms.visibility import apply_session_transforms, is_enabled
from fastmcp.settings import DuplicateBehavior as DuplicateBehaviorSetting
from fastmcp.settings import Settings
from fastmcp.tools.function_tool import FunctionTool
from fastmcp.tools.tool import AuthCheckCallable, Tool, ToolResult
from fastmcp.tools.tool import Tool, ToolResult
from fastmcp.tools.tool_transform import ToolTransformConfig
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.logging import get_logger
@ -99,7 +96,6 @@ if TYPE_CHECKING:
from fastmcp.server.providers.openapi import RouteMap
from fastmcp.server.providers.openapi import RouteMapFn as OpenAPIRouteMapFn
from fastmcp.server.providers.proxy import FastMCPProxy
from fastmcp.tools.tool import ToolResultSerializerType
logger = get_logger(__name__)
@ -107,39 +103,37 @@ logger = get_logger(__name__)
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
def _resolve_on_duplicate(
on_duplicate: DuplicateBehavior | None,
on_duplicate_tools: DuplicateBehavior | None,
on_duplicate_resources: DuplicateBehavior | None,
on_duplicate_prompts: DuplicateBehavior | None,
) -> DuplicateBehavior:
"""Resolve on_duplicate from deprecated per-type params.
_REMOVED_KWARGS: dict[str, str] = {
"host": "Pass `host` to `run_http_async()`, or set FASTMCP_HOST.",
"port": "Pass `port` to `run_http_async()`, or set FASTMCP_PORT.",
"sse_path": "Pass `path` to `run_http_async()` or `http_app()`, or set FASTMCP_SSE_PATH.",
"message_path": "Set FASTMCP_MESSAGE_PATH.",
"streamable_http_path": "Pass `path` to `run_http_async()` or `http_app()`, or set FASTMCP_STREAMABLE_HTTP_PATH.",
"json_response": "Pass `json_response` to `run_http_async()` or `http_app()`, or set FASTMCP_JSON_RESPONSE.",
"stateless_http": "Pass `stateless_http` to `run_http_async()` or `http_app()`, or set FASTMCP_STATELESS_HTTP.",
"debug": "Set FASTMCP_DEBUG.",
"log_level": "Pass `log_level` to `run_http_async()`, or set FASTMCP_LOG_LEVEL.",
"on_duplicate_tools": "Use `on_duplicate=` instead.",
"on_duplicate_resources": "Use `on_duplicate=` instead.",
"on_duplicate_prompts": "Use `on_duplicate=` instead.",
"tool_serializer": "Return ToolResult from your tools instead. See https://gofastmcp.com/servers/tools#custom-serialization",
"include_tags": "Use `server.enable(tags=..., only=True)` after creating the server.",
"exclude_tags": "Use `server.disable(tags=...)` after creating the server.",
"tool_transformations": "Use `server.add_transform(ToolTransform(...))` after creating the server.",
}
Takes the most strict value if multiple are provided.
Delete this function when removing deprecated params.
"""
strictness_order: list[DuplicateBehavior] = ["error", "warn", "replace", "ignore"]
deprecated_values: list[DuplicateBehavior] = []
deprecated_params: list[tuple[str, DuplicateBehavior | None]] = [
("on_duplicate_tools", on_duplicate_tools),
("on_duplicate_resources", on_duplicate_resources),
("on_duplicate_prompts", on_duplicate_prompts),
]
for name, value in deprecated_params:
if value is not None:
if fastmcp.settings.deprecation_warnings:
warnings.warn(
f"{name} is deprecated, use on_duplicate instead",
DeprecationWarning,
stacklevel=4,
)
deprecated_values.append(value)
if on_duplicate is None and deprecated_values:
return min(deprecated_values, key=lambda x: strictness_order.index(x))
return on_duplicate or "warn"
def _check_removed_kwargs(kwargs: dict[str, Any]) -> None:
"""Raise helpful TypeErrors for kwargs removed in v3."""
for key in kwargs:
if key in _REMOVED_KWARGS:
raise TypeError(
f"FastMCP() no longer accepts `{key}`. {_REMOVED_KWARGS[key]}"
)
if kwargs:
raise TypeError(
f"FastMCP() got unexpected keyword argument(s): {', '.join(repr(k) for k in kwargs)}"
)
Transport = Literal["stdio", "http", "sse", "streamable-http"]
@ -232,45 +226,24 @@ class FastMCP(
middleware: Sequence[Middleware] | None = None,
providers: Sequence[Provider] | None = None,
lifespan: LifespanCallable | Lifespan | None = None,
mask_error_details: bool | None = None,
tools: Sequence[Tool | Callable[..., Any]] | None = None,
tool_serializer: ToolResultSerializerType | None = None,
include_tags: Collection[str] | None = None,
exclude_tags: Collection[str] | None = None,
on_duplicate: DuplicateBehavior | None = None,
mask_error_details: bool | None = None,
dereference_schemas: bool = True,
strict_input_validation: bool | None = None,
list_page_size: int | None = None,
tasks: bool | None = None,
session_state_store: AsyncKeyValue | None = None,
# ---
# --- DEPRECATED parameters ---
# ---
on_duplicate_tools: DuplicateBehavior | None = None,
on_duplicate_resources: DuplicateBehavior | None = None,
on_duplicate_prompts: DuplicateBehavior | None = None,
log_level: str | None = None,
debug: bool | None = None,
host: str | None = None,
port: int | None = None,
sse_path: str | None = None,
message_path: str | None = None,
streamable_http_path: str | None = None,
json_response: bool | None = None,
stateless_http: bool | None = None,
sampling_handler: SamplingHandler | None = None,
sampling_handler_behavior: Literal["always", "fallback"] | None = None,
tool_transformations: Mapping[str, ToolTransformConfig] | None = None,
**kwargs: Any,
):
_check_removed_kwargs(kwargs)
# Initialize Provider (sets up _transforms)
super().__init__()
# Resolve on_duplicate from deprecated params (delete when removing deprecation)
self._on_duplicate: DuplicateBehaviorSetting = _resolve_on_duplicate(
on_duplicate,
on_duplicate_tools,
on_duplicate_resources,
on_duplicate_prompts,
)
self._on_duplicate: DuplicateBehaviorSetting = on_duplicate or "warn"
# Resolve server default for background task support
self._support_tasks_by_default: bool = tasks if tasks is not None else False
@ -312,16 +285,6 @@ class FastMCP(
raise ValueError("list_page_size must be a positive integer")
self._list_page_size: int | None = list_page_size
if tool_serializer is not None and fastmcp.settings.deprecation_warnings:
warnings.warn(
"The `tool_serializer` parameter is deprecated. "
"Return ToolResult from your tools for full control over serialization. "
"See https://gofastmcp.com/servers/tools#custom-serialization for migration examples.",
DeprecationWarning,
stacklevel=2,
)
self._tool_serializer: Callable[[Any], str] | None = tool_serializer
# Handle Lifespan instances (they're callable) or regular lifespan functions
if lifespan is not None:
self._lifespan: LifespanCallable[LifespanResultT] = lifespan
@ -349,38 +312,9 @@ class FastMCP(
if tools:
for tool in tools:
if not isinstance(tool, Tool):
tool = Tool.from_function(tool, serializer=self._tool_serializer)
tool = Tool.from_function(tool)
self.add_tool(tool)
# Handle deprecated include_tags and exclude_tags parameters
if include_tags is not None:
warnings.warn(
"include_tags is deprecated. Use server.enable(tags=..., only=True) instead.",
DeprecationWarning,
stacklevel=2,
)
# For backwards compatibility, initialize allowlist from include_tags
self.enable(tags=set(include_tags), only=True)
if exclude_tags is not None:
warnings.warn(
"exclude_tags is deprecated. Use server.disable(tags=...) instead.",
DeprecationWarning,
stacklevel=2,
)
# For backwards compatibility, initialize blocklist from exclude_tags
self.disable(tags=set(exclude_tags))
# Handle deprecated tool_transformations parameter
if tool_transformations:
if fastmcp.settings.deprecation_warnings:
warnings.warn(
"The tool_transformations parameter is deprecated. Use "
"server.add_transform(ToolTransform({...})) instead.",
DeprecationWarning,
stacklevel=2,
)
self._transforms.append(ToolTransform(dict(tool_transformations)))
self.strict_input_validation: bool = (
strict_input_validation
if strict_input_validation is not None
@ -389,6 +323,13 @@ class FastMCP(
self.middleware: list[Middleware] = list(middleware or [])
if dereference_schemas:
from fastmcp.server.middleware.dereference import (
DereferenceRefsMiddleware,
)
self.middleware.append(DereferenceRefsMiddleware())
# Set up MCP protocol handlers
self._setup_handlers()
@ -397,71 +338,9 @@ class FastMCP(
sampling_handler_behavior or "fallback"
)
self._handle_deprecated_settings(
log_level=log_level,
debug=debug,
host=host,
port=port,
sse_path=sse_path,
message_path=message_path,
streamable_http_path=streamable_http_path,
json_response=json_response,
stateless_http=stateless_http,
)
def __repr__(self) -> str:
return f"{type(self).__name__}({self.name!r})"
def _handle_deprecated_settings(
self,
log_level: str | None,
debug: bool | None,
host: str | None,
port: int | None,
sse_path: str | None,
message_path: str | None,
streamable_http_path: str | None,
json_response: bool | None,
stateless_http: bool | None,
) -> None:
"""Handle deprecated settings. Deprecated in 2.8.0."""
deprecated_settings: dict[str, Any] = {}
for name, arg in [
("log_level", log_level),
("debug", debug),
("host", host),
("port", port),
("sse_path", sse_path),
("message_path", message_path),
("streamable_http_path", streamable_http_path),
("json_response", json_response),
("stateless_http", stateless_http),
]:
if arg is not None:
# Deprecated in 2.8.0
if fastmcp.settings.deprecation_warnings:
warnings.warn(
f"Providing `{name}` when creating a server is deprecated. Provide it when calling `run` or as a global setting instead.",
DeprecationWarning,
stacklevel=2,
)
deprecated_settings[name] = arg
combined_settings = fastmcp.settings.model_dump() | deprecated_settings
self._deprecated_settings = Settings(**combined_settings)
@property
def settings(self) -> Settings:
# Deprecated in 2.8.0
if fastmcp.settings.deprecation_warnings:
warnings.warn(
"Accessing `.settings` on a FastMCP instance is deprecated. Use the global `fastmcp.settings` instead.",
DeprecationWarning,
stacklevel=2,
)
return self._deprecated_settings
@property
def name(self) -> str:
return self._mcp_server.name
@ -489,6 +368,18 @@ class FastMCP(
else:
return list(self._mcp_server.icons)
@property
def local_provider(self) -> LocalProvider:
"""The server's local provider, which stores directly-registered components.
Use this to remove components:
mcp.local_provider.remove_tool("my_tool")
mcp.local_provider.remove_resource("data://info")
mcp.local_provider.remove_prompt("my_prompt")
"""
return self._local_provider
async def _run_middleware(
self,
context: MiddlewareContext[Any],
@ -638,7 +529,7 @@ class FastMCP(
if not skip_auth and tool.auth is not None:
ctx = AuthContext(token=token, component=tool)
try:
if not run_auth_checks(tool.auth, ctx):
if not await run_auth_checks(tool.auth, ctx):
continue
except AuthorizationError:
continue
@ -669,7 +560,7 @@ class FastMCP(
if not skip_auth and tool.auth is not None:
ctx = AuthContext(token=token, component=tool)
try:
if not run_auth_checks(tool.auth, ctx):
if not await run_auth_checks(tool.auth, ctx):
return None
except AuthorizationError:
return None
@ -736,7 +627,7 @@ class FastMCP(
if not skip_auth and resource.auth is not None:
ctx = AuthContext(token=token, component=resource)
try:
if not run_auth_checks(resource.auth, ctx):
if not await run_auth_checks(resource.auth, ctx):
continue
except AuthorizationError:
continue
@ -767,7 +658,7 @@ class FastMCP(
if not skip_auth and resource.auth is not None:
ctx = AuthContext(token=token, component=resource)
try:
if not run_auth_checks(resource.auth, ctx):
if not await run_auth_checks(resource.auth, ctx):
return None
except AuthorizationError:
return None
@ -835,7 +726,7 @@ class FastMCP(
if not skip_auth and template.auth is not None:
ctx = AuthContext(token=token, component=template)
try:
if not run_auth_checks(template.auth, ctx):
if not await run_auth_checks(template.auth, ctx):
continue
except AuthorizationError:
continue
@ -866,7 +757,7 @@ class FastMCP(
if not skip_auth and template.auth is not None:
ctx = AuthContext(token=token, component=template)
try:
if not run_auth_checks(template.auth, ctx):
if not await run_auth_checks(template.auth, ctx):
return None
except AuthorizationError:
return None
@ -930,7 +821,7 @@ class FastMCP(
if not skip_auth and prompt.auth is not None:
ctx = AuthContext(token=token, component=prompt)
try:
if not run_auth_checks(prompt.auth, ctx):
if not await run_auth_checks(prompt.auth, ctx):
continue
except AuthorizationError:
continue
@ -961,7 +852,7 @@ class FastMCP(
if not skip_auth and prompt.auth is not None:
ctx = AuthContext(token=token, component=prompt)
try:
if not run_auth_checks(prompt.auth, ctx):
if not await run_auth_checks(prompt.auth, ctx):
return None
except AuthorizationError:
return None
@ -1378,6 +1269,9 @@ class FastMCP(
def remove_tool(self, name: str, version: str | None = None) -> None:
"""Remove tool(s) from the server.
.. deprecated::
Use ``mcp.local_provider.remove_tool(name)`` instead.
Args:
name: The name of the tool to remove.
version: If None, removes ALL versions. If specified, removes only that version.
@ -1385,6 +1279,13 @@ class FastMCP(
Raises:
NotFoundError: If no matching tool is found.
"""
if fastmcp.settings.deprecation_warnings:
warnings.warn(
"remove_tool() is deprecated. Use "
"mcp.local_provider.remove_tool(name) instead.",
DeprecationWarning,
stacklevel=2,
)
try:
self._local_provider.remove_tool(name, version)
except KeyError:
@ -1412,7 +1313,7 @@ class FastMCP(
app: AppConfig | dict[str, Any] | bool | None = None,
task: bool | TaskConfig | None = None,
timeout: float | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> FunctionTool: ...
@overload
@ -1433,7 +1334,7 @@ class FastMCP(
app: AppConfig | dict[str, Any] | bool | None = None,
task: bool | TaskConfig | None = None,
timeout: float | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> Callable[[AnyFunction], FunctionTool]: ...
def tool(
@ -1453,7 +1354,7 @@ class FastMCP(
app: AppConfig | dict[str, Any] | bool | None = None,
task: bool | TaskConfig | None = None,
timeout: float | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> (
Callable[[AnyFunction], FunctionTool]
| FunctionTool
@ -1530,7 +1431,6 @@ class FastMCP(
meta=meta,
task=task if task is not None else self._support_tasks_by_default,
timeout=timeout,
serializer=self._tool_serializer,
auth=auth,
)
@ -1575,7 +1475,7 @@ class FastMCP(
meta: dict[str, Any] | None = None,
app: AppConfig | dict[str, Any] | bool | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> Callable[[AnyFunction], Resource | ResourceTemplate | AnyFunction]:
"""Decorator to register a function as a resource.
@ -1706,7 +1606,7 @@ class FastMCP(
tags: set[str] | None = None,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> FunctionPrompt: ...
@overload
@ -1722,7 +1622,7 @@ class FastMCP(
tags: set[str] | None = None,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> Callable[[AnyFunction], FunctionPrompt]: ...
def prompt(
@ -1737,7 +1637,7 @@ class FastMCP(
tags: set[str] | None = None,
meta: dict[str, Any] | None = None,
task: bool | TaskConfig | None = None,
auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> (
Callable[[AnyFunction], FunctionPrompt]
| FunctionPrompt
@ -2029,6 +1929,7 @@ class FastMCP(
mcp_component_fn: OpenAPIComponentFn | None = None,
mcp_names: dict[str, str] | None = None,
tags: set[str] | None = None,
validate_output: bool = True,
**settings: Any,
) -> Self:
"""
@ -2045,6 +1946,10 @@ class FastMCP(
mcp_component_fn: Optional callable for component customization
mcp_names: Optional dictionary mapping operationId to component names
tags: Optional set of tags to add to all components
validate_output: If True (default), tools use the output schema
extracted from the OpenAPI spec for response validation. If
False, a permissive schema is used instead, allowing any
response structure while still returning structured JSON.
**settings: Additional settings passed to FastMCP
Returns:
@ -2060,6 +1965,7 @@ class FastMCP(
mcp_component_fn=mcp_component_fn,
mcp_names=mcp_names,
tags=tags,
validate_output=validate_output,
)
return cls(name=name, providers=[provider], **settings)

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