mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
Merge origin/main into feature/apps
This commit is contained in:
commit
ccb74da45c
162 changed files with 8818 additions and 2836 deletions
95
.github/actions/run-claude/action.yml
vendored
Normal file
95
.github/actions/run-claude/action.yml
vendored
Normal 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 }}"}
|
||||
62
.github/scripts/mention/gh-get-review-threads.sh
vendored
Executable file
62
.github/scripts/mention/gh-get-review-threads.sh
vendored
Executable 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
|
||||
61
.github/scripts/mention/gh-resolve-review-thread.sh
vendored
Executable file
61
.github/scripts/mention/gh-resolve-review-thread.sh
vendored
Executable 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
251
.github/scripts/pr-review/pr-comment.sh
vendored
Executable 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
128
.github/scripts/pr-review/pr-diff.sh
vendored
Executable 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
|
||||
190
.github/scripts/pr-review/pr-existing-comments.sh
vendored
Executable file
190
.github/scripts/pr-review/pr-existing-comments.sh
vendored
Executable 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"))
|
||||
'
|
||||
84
.github/scripts/pr-review/pr-remove-comment.sh
vendored
Executable file
84
.github/scripts/pr-review/pr-remove-comment.sh
vendored
Executable 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
143
.github/scripts/pr-review/pr-review.sh
vendored
Executable 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
|
||||
178
.github/workflows/martian-issue-triage.yml
vendored
178
.github/workflows/martian-issue-triage.yml
vendored
|
|
@ -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 }}"
|
||||
}
|
||||
204
.github/workflows/martian-triage-issue.yml
vendored
Normal file
204
.github/workflows/martian-triage-issue.yml
vendored
Normal 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>
|
||||
144
.github/workflows/marvin-comment-on-issue.yml
vendored
Normal file
144
.github/workflows/marvin-comment-on-issue.yml
vendored
Normal 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>
|
||||
284
.github/workflows/marvin-comment-on-pr.yml
vendored
Normal file
284
.github/workflows/marvin-comment-on-pr.yml
vendored
Normal 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>
|
||||
87
.github/workflows/marvin.yml
vendored
87
.github/workflows/marvin.yml
vendored
|
|
@ -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."
|
||||
}
|
||||
70
.github/workflows/update-config-schema.yml
vendored
70
.github/workflows/update-config-schema.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
58
.github/workflows/update-sdk-docs.yml
vendored
58
.github/workflows/update-sdk-docs.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'`
|
||||
|
||||
|
|
|
|||
|
|
@ -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)**
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/).
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
35
docs/python-sdk/fastmcp-server-middleware-dereference.mdx
Normal file
35
docs/python-sdk/fastmcp-server-middleware-dereference.mdx
Normal 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]
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
113
docs/python-sdk/fastmcp-server-tasks-notifications.mdx
Normal file
113
docs/python-sdk/fastmcp-server-tasks-notifications.mdx
Normal 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).
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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__":
|
||||
|
|
|
|||
82
examples/task_elicitation.py
Normal file
82
examples/task_elicitation.py
Normal 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())
|
||||
105
examples/testing_demo/uv.lock
generated
105
examples/testing_demo/uv.lock
generated
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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..."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from fastmcp.server.dependencies import (
|
|||
CurrentWorker,
|
||||
Progress,
|
||||
ProgressLike,
|
||||
TokenClaim,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
|
@ -39,4 +40,5 @@ __all__ = [
|
|||
"Depends",
|
||||
"Progress",
|
||||
"ProgressLike",
|
||||
"TokenClaim",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
||||
|
|
|
|||
78
src/fastmcp/server/middleware/dereference.py
Normal file
78
src/fastmcp/server/middleware/dereference.py
Normal 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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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]] = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue