mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-27 07:50:43 +02:00
Publish FastMCP v4.0.0b3 docs (#4842)
This commit is contained in:
parent
0e941cf051
commit
0a619de571
179 changed files with 12458 additions and 1433 deletions
7
.github/actions/run-claude/action.yml
vendored
7
.github/actions/run-claude/action.yml
vendored
|
|
@ -37,6 +37,11 @@ inputs:
|
|||
required: false
|
||||
default: ""
|
||||
|
||||
extra-allowed-tools:
|
||||
description: "Additional comma-separated tools to append to allowed-tools"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
model:
|
||||
description: "Model to use for Claude"
|
||||
required: false
|
||||
|
|
@ -88,7 +93,7 @@ runs:
|
|||
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.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: |
|
||||
|
|
|
|||
10
.github/actions/run-pytest/action.yml
vendored
10
.github/actions/run-pytest/action.yml
vendored
|
|
@ -46,6 +46,16 @@ runs:
|
|||
PARALLEL_FLAGS="--numprocesses auto --maxprocesses $MAX_PROCS --dist worksteal"
|
||||
fi
|
||||
|
||||
# pytest-timeout has no signal-based method on Windows, so it falls back
|
||||
# to the thread method, which dumps stacks and os._exit()s the process.
|
||||
# Under a contended runner that turns a single slow test into a dead
|
||||
# xdist worker, failing whichever unrelated test that worker happened to
|
||||
# be running. Give parallel Windows runs more headroom so ordinary
|
||||
# scheduling jitter does not take a worker down.
|
||||
if [ "$RUNNER_OS" == "Windows" ] && [ "$MAX_PROCS" != "0" ]; then
|
||||
TIMEOUT=$((TIMEOUT * 4))
|
||||
fi
|
||||
|
||||
uv run --no-sync pytest \
|
||||
--inline-snapshot=disable \
|
||||
--timeout=$TIMEOUT \
|
||||
|
|
|
|||
14
.github/dependabot.yml
vendored
14
.github/dependabot.yml
vendored
|
|
@ -1,14 +0,0 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "dependencies"
|
||||
38
.github/workflows/publish-fastmcp.yml
vendored
38
.github/workflows/publish-fastmcp.yml
vendored
|
|
@ -178,19 +178,27 @@ jobs:
|
|||
run: uv publish -v dist/fastmcp-*.tar.gz dist/fastmcp-*.whl
|
||||
|
||||
update-published-docs:
|
||||
name: Update published-docs branch
|
||||
name: Open published-docs PR
|
||||
runs-on: ubuntu-latest
|
||||
needs: pypi-publish
|
||||
if: github.event_name == 'workflow_run' && github.event.workflow_run.event == 'release' && needs['pypi-publish'].outputs.is_prerelease != 'true'
|
||||
timeout-minutes: 2
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Generate Marvin App token
|
||||
id: marvin-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ secrets.MARVIN_APP_ID }}
|
||||
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ steps.marvin-token.outputs.token }}
|
||||
|
||||
- name: Check release line
|
||||
id: release_line
|
||||
|
|
@ -205,6 +213,26 @@ jobs:
|
|||
echo "Release commit is not on ${DEFAULT_BRANCH}; skipping published-docs update."
|
||||
fi
|
||||
|
||||
- name: Point published-docs at published release
|
||||
- name: Prepare published docs tree
|
||||
if: steps.release_line.outputs.update_published_docs == 'true'
|
||||
run: git push --force origin "HEAD:published-docs"
|
||||
env:
|
||||
RELEASE_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
git fetch origin published-docs
|
||||
git switch --force-create published-docs-sync origin/published-docs
|
||||
git read-tree --reset -u "$RELEASE_SHA"
|
||||
test "$(git write-tree)" = "$(git rev-parse "${RELEASE_SHA}^{tree}")"
|
||||
|
||||
- name: Open published docs PR
|
||||
if: steps.release_line.outputs.update_published_docs == 'true'
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ steps.marvin-token.outputs.token }}
|
||||
base: published-docs
|
||||
branch: marvin/publish-docs-v${{ needs.pypi-publish.outputs.version }}
|
||||
commit-message: "Publish FastMCP v${{ needs.pypi-publish.outputs.version }} docs"
|
||||
title: "Publish FastMCP v${{ needs.pypi-publish.outputs.version }} docs"
|
||||
body: "Updates `published-docs` to the exact release tree. Merging publishes the documentation to production."
|
||||
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>"
|
||||
|
|
|
|||
9
.github/workflows/require-issue-link.yml
vendored
9
.github/workflows/require-issue-link.yml
vendored
|
|
@ -379,9 +379,6 @@ jobs:
|
|||
async function enforceFailure(kind) {
|
||||
await addLabel();
|
||||
|
||||
const reason = kind === 'no-link'
|
||||
? "it doesn't reference a tracked issue assigned to you"
|
||||
: "you aren't assigned to the issue it references";
|
||||
const steps = kind === 'no-link'
|
||||
? [
|
||||
`1. Find or [open an issue](https://github.com/${owner}/${repo}/issues/new/choose) describing the change — if you open it, you have first claim on it.`,
|
||||
|
|
@ -393,9 +390,9 @@ jobs:
|
|||
|
||||
const commentBody = [
|
||||
MARKER,
|
||||
"**Don't open a new pull request — this one reopens on its own.** It's closed for " +
|
||||
`now because ${reason}, but the moment that's fixed it reopens automatically. Keep this ` +
|
||||
'PR and edit it; opening a fresh duplicate just starts you over and creates more to triage.',
|
||||
'**This pull request was closed because it must link to an issue assigned to you.** ' +
|
||||
'Once this PR links to an issue and a maintainer assigns that issue to you, it will ' +
|
||||
'reopen automatically. Please update this PR rather than opening a new one.',
|
||||
'',
|
||||
`Per [CONTRIBUTING.md](https://github.com/${owner}/${repo}/blob/main/CONTRIBUTING.md), an external PR must reference an issue that's assigned to its author. To get there:`,
|
||||
'',
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ When modifying MCP functionality, changes typically need to be applied across al
|
|||
|
||||
**Read `CONTRIBUTING.md` before opening issues or PRs.** It describes when PRs are appropriate, what we expect from enhancement proposals, and what we'll close without review.
|
||||
|
||||
**Review closed contributor PRs.** When reviewing an issue, inspect every associated non-maintainer PR, including closed PRs. External PRs may be closed as part of the issue-link and assignment workflow, so closure alone is not a negative signal. Read `CONTRIBUTING.md` and the PR timeline and comments to understand its status before evaluating it.
|
||||
|
||||
### Git & CI
|
||||
|
||||
- Prek hooks are required (run automatically on commits)
|
||||
|
|
@ -117,7 +119,9 @@ Set `target_commitish` to the same branch that will receive the release tag. For
|
|||
|
||||
**Patch releases** (3.1.1, 3.0.2) get 1-2 sentences explaining what broke and what the fix does. Keep it minimal — the auto-generated changelog has the details.
|
||||
|
||||
**Merge the docs changelog PR *before* cutting the release, not after.** The post-publish `update-published-docs` job force-pushes the `published-docs` branch (which gofastmcp.com serves) to the released commit for stable releases on the default branch, so the changelog entry only reaches the live site if it's already in the commit being tagged. Land the docs PR on the release target branch first, then cut the release from that branch. If you tag first and merge docs after, this release's changelog won't appear on the live site until the next default-branch stable release force-pushes `published-docs` forward. Maintenance releases from `release/3.x` or `release/2.x` publish packages and GitHub notes without repointing `published-docs`; add their changelog entries on the maintenance branch, slotted into the matching major-version section. Two hand-maintained files mirror the GitHub release and must get a new entry for every version, newest at the top (these are `.mdx` and are not covered by the prek Prettier hook, which only runs on `yaml`/`json5` — match the existing entries' style by hand):
|
||||
**Publish docs through a PR.** The `published-docs` branch serves gofastmcp.com, and repository rules reject direct pushes and force-pushes to it. Stable releases from `main` automatically open a publication PR after PyPI succeeds. For prereleases and later docs follow-ups, create the same PR manually: start a temporary branch from the current `published-docs`, make a single commit whose tree exactly matches the desired commit on `main`, and use `published-docs` as the PR base. Merging publishes to production. Never push directly to `published-docs`.
|
||||
|
||||
**Merge the docs changelog PR *before* cutting the release, not after.** The post-publish `update-published-docs` job opens a PR that syncs `published-docs` to the released commit for stable releases on the default branch, so the changelog entry only reaches the live site if it's already in the commit being tagged. Land the docs PR on the release target branch first, then cut the release from that branch. If you tag first and merge docs after, this release's publication PR will not include the changelog; publish `main` manually through the PR flow above or wait for the next default-branch stable release. Maintenance releases from `release/3.x` or `release/2.x` publish packages and GitHub notes without repointing `published-docs`; add their changelog entries on the maintenance branch, slotted into the matching major-version section. Two hand-maintained files mirror the GitHub release and must get a new entry for every version, newest at the top (these are `.mdx` and are not covered by the prek Prettier hook, which only runs on `yaml`/`json5` — match the existing entries' style by hand):
|
||||
|
||||
- `docs/changelog.mdx` is the full mirror. Add an `<Update label="v<version>" description="YYYY-MM-DD">` block with: a bold linked title (`**[v<version>: <pun>](<release-url>)**`), a condensed 1-paragraph intro (one sentence for patches), the full categorized PR list reformatted from the `--generate-notes` output (`* <title> by [@user](https://github.com/user) in [#NNNN](<pull-url>)`), a `## New Contributors` list (plain `@user`, linked PR), and a `**Full Changelog**: [vA...vB](<compare-url>)` line.
|
||||
- `docs/updates.mdx` is the skimmable card feed. Add an `<Update label="FastMCP <version>" description="Month DD, YYYY" tags={["Releases"]}>` wrapping a `<Card>` that links to the GitHub release, with a 1-2 sentence summary and (for point releases) a handful of emoji-bulleted highlights.
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ FastMCP v4.0 is an engine swap. Three forces drive the major version:
|
|||
|
||||
## Release strategy
|
||||
|
||||
The migration merges to `main` and development continues there with subsequent PRs. Releases follow the SDK's own beta timeline:
|
||||
The migration lives on `main`, which now depends on the stable MCP Python SDK 2.0 line. FastMCP continues cutting prereleases while the v4 APIs soak, then ships 4.0.0 from the same branch.
|
||||
|
||||
- **`main` carries the beta pins.** While the SDK is on `mcp==2.0.0b1` / `mcp-types==2.0.0b1`, `main` cuts **pre-releases** (`4.0.0b1`, `4.0.0b2`, …). No stable PyPI release goes out until `mcp 2.0.0` reaches GA — at which point the pins swap to the stable SDK and `4.0.0` ships. The pin-swap is a tracked checklist item on the [Known Gaps](known-gaps.md) page.
|
||||
- **`main` owns FastMCP 4.** It carries stable `mcp>=2.0.0` and `mcp-types>=2.0.0` dependencies. Beta 3 is the current prerelease target; the [Known Gaps](known-gaps.md) page tracks the remaining decisions before 4.0.0.
|
||||
- **`release/3.x` is the maintenance line.** A `release/3.x` branch is cut from pre-merge `main`. It stays on the SDK v1 line, receives upstream security patches, and serves users who cannot move to the SDK v2 beta yet.
|
||||
|
||||
### Release codenames
|
||||
|
|
@ -34,8 +34,9 @@ Following the pun-title convention (`v<version>: <pun>`), the v4 line runs a sin
|
|||
| `4.0.0a1` (alpha) | **Fourst Contact** | _first contact_ — the first, cautious look at the new engine |
|
||||
| `4.0.0a2` (alpha) | **Back and Fourth** | _back and forth_ — the second pass, where background tasks and stateless state land |
|
||||
| `4.0.0b1` (beta) | **Fourgone Conclusion** | _foregone conclusion_ — once the MCP SDK went v2, v4 was inevitable |
|
||||
| `4.0.0b2` (beta) | **Fourmidable** | _formidable_ — held in reserve for a second beta if one is needed |
|
||||
| `4.0.0` (stable) | **Fast Fourward** | _fast forward_ — full speed onto the new foundation |
|
||||
| `4.0.0b2` (beta) | **Four the Better** | _for the better_ — a hardening release focused on correctness, compatibility, and security |
|
||||
| `4.0.0b3` (beta) | **Fast Fourward** | _fast forward_ — the final beta carries the accumulated v4 work into its GA soak |
|
||||
| `4.0.0` (stable) | **Fourmidable** | _formidable_ — the stable release of the new protocol foundation |
|
||||
|
||||
## How to read the register
|
||||
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@
|
|||
title: Known Gaps and Upstream Dependencies
|
||||
---
|
||||
|
||||
The migration ships with a set of deliberate gaps: temporary shims, xfailed tests, and pins that depend on the MCP Python SDK v2 reaching GA. Each is tracked here with its removal trigger. This page is the checklist for the beta-to-stable transition and the advisory relationship with the SDK team.
|
||||
The migration ships with a small set of deliberate compatibility boundaries and expected test gaps. FastMCP now depends on the stable MCP Python SDK 2.0 line; this page tracks what remains for the beta-to-stable transition and the advisory relationship with the SDK team.
|
||||
|
||||
## The xfail register
|
||||
|
||||
Roughly forty `xfail` markers across the test tree name the SDK gaps and removed protocol surfaces they wait on. Re-running the suite against a new SDK beta surfaces which have closed (a strict xfail that starts passing fails the suite, prompting removal of the marker). They cluster in three areas — but the largest cluster is no longer a set of gaps to close.
|
||||
The unit suite has three expected xfails. Two are strict SDK compatibility checks, so an upstream fix turns them into failures and prompts us to remove the markers.
|
||||
|
||||
**Task suite (`tests/server/tasks/`, `tests/client/tasks/`) — SEP-1686 wire layer being removed; engine rebuilt on SEP-2663.** The large majority. These cover the 2025 task protocol (SEP-1686), which left the core MCP spec and was reworked into the `io.modelcontextprotocol/tasks` extension (SEP-2663). FastMCP's SEP-1686 *wire* machinery (capability advertisement, the `tasks/get|result|list|cancel` handlers, the push notification/elicitation relay) is slated for removal, so the wire-protocol xfails disappear with the code they cover — they are not waiting on an SDK fix. The Docket/Redis *execution engine* underneath is not discarded: it is extracted into the planned `fastmcp-tasks` package and re-adapted to the SEP-2663 polling shape (see [Background Tasks (SEP-2663)](background-tasks.md)). The two SDK gaps these were originally filed against — **sdk-feedback #1** (SEP-1686 task result types omitted from the method registries) and **sdk-feedback #3** (no `task` field on `ReadResourceRequestParams` / `GetPromptRequestParams`) — are moot: they patched the SEP-1686 wire shape, which SEP-2663 replaces with a `CreateTaskResult` claimed on `tools/call`. The gap that matters for the rebuild is **sdk-feedback #2** (extensions capability stripped at pre-2026 negotiated versions) — it now gates a flagship feature and is escalated accordingly.
|
||||
**Stateless HTTP elicitation (`tests/client/test_streamable_http.py`).** One parametrized case exercises server-initiated elicitation over stateless HTTP. The sessionless protocol has no server-to-client back-channel, so the case is expected to xfail by construction. Guard-mode elicitation is the supported modern path.
|
||||
|
||||
**Protocol eras (`tests/server/test_protocol_eras.py`).** One remaining strict xfail, and it too is task-related: the v2 SDK high-level client exposes no `task=` parameter on `call_tool`, so a SEP-1686 task-augmented `tools/call` cannot be submitted through it. It resolves with the SEP-1686 wire-layer removal above; the SEP-2663 rebuild submits tasks by advertising the extension capability and claiming a `CreateTaskResult`, not through a `task=` params field. The earlier strict xfail for the `ctx.elicit` / `ctx.sample` "Method not found" degradation (sdk-feedback #10) is **gone** — the era-gating shipped in #4448 flipped it to a passing test.
|
||||
**MCP Apps (`tests/test_apps.py`).** Two strict xfails track **sdk-feedback #2**: the SDK strips `capabilities.extensions` at pre-2026 negotiated versions, so the UI extension cannot be advertised to legacy-era clients. Modern clients receive the extension normally.
|
||||
|
||||
**MCP Apps (`tests/test_apps.py`).** Two xfails tied to **sdk-feedback #2** — the `extensions` capability is stripped by the pre-2026 version sieve, so the UI extension can't be advertised to legacy-era clients.
|
||||
Credential-gated GitHub integration suites also use conditional xfail markers when their environment variables are absent. Those are test-environment controls rather than product gaps and are not part of the GA decision.
|
||||
|
||||
## Shims and their removal triggers
|
||||
|
||||
|
|
@ -20,15 +20,12 @@ Every shim in the migration is temporary and carries a documented removal trigge
|
|||
|
||||
| Shim | Location | Removal trigger |
|
||||
| --- | --- | --- |
|
||||
| `_sdk_patches.py` — task registry widening | `fastmcp_slim/fastmcp/_sdk_patches.py` | Removed with FastMCP's SEP-1686 wire machinery (`server/tasks/`), which is slated for removal now that the 2025 task protocol left the spec. The SEP-2663 rebuild does not need it — `CreateTaskResult` is claimed on `tools/call` through the extensions mechanism, which the SDK registries already admit. |
|
||||
| `_compat.py` — camelCase field bridge | `fastmcp_slim/fastmcp/_compat.py` | User-migration aid; removed in a future release after users migrate reads to snake_case. Users can preview removal with `mcp_camelcase_compat = False`. |
|
||||
| `FastMCPRequestContext` ContextVar | `fastmcp_slim/fastmcp/server/dependencies.py` | The SDK deliberately passes context as an argument with no ContextVar; FastMCP's public `get_context()` needs ambient access, and the shim also lifts `_meta`, which the SDK's `TypedDict` drops. No planned removal — this is a permanent boundary, not a beta gap. |
|
||||
| `FastMCPServerMiddleware` | `fastmcp_slim/fastmcp/server/low_level.py` | Already the native SDK `ServerMiddleware` path; no cleaner hook exists. Permanent. |
|
||||
| Client `get_session_id` header sniff | `fastmcp_slim/fastmcp/client/transports/http.py` | SDK exposes session id (or an `on_session_created` callback) from `streamable_http_client`, at parity with `sse_client` (sdk-feedback #5). |
|
||||
| `_sdk_context_shim.py` — generic handler aliases | `fastmcp_slim/fastmcp/client/_sdk_context_shim.py` | The SDK's `ClientRequestContext` is not subscriptable, so FastMCP keeps the public generic `SamplingHandler`/`RootsHandler`/`ElicitationHandler` aliases. Permanent unless the SDK makes the context subscriptable (sdk-feedback #7). |
|
||||
|
||||
The `TaskNotificationHandler` binding (sdk-feedback #8) is the client-side equivalent: it registers a `NotificationBinding` for the SEP-1686 `notifications/tasks/status` because the SDK no longer tees custom server notifications to the message handler. It goes away with the SEP-1686 wire machinery it serves; the `fastmcp-tasks` client half registers its own binding for the SEP-2663 `notifications/tasks` shape when it ships (push notifications are deferred to a later `fastmcp-tasks` version — v1 is polling-only).
|
||||
|
||||
## Statelessness on 2026-07-28
|
||||
|
||||
The `2026-07-28` era is stateless by protocol construction, and the recurring maintainer question is whether that statelessness has to be woven through FastMCP everywhere. It does not — but the honest accounting has three parts: features that are legacy-only because the protocol removed the mechanism, features that already work because they never relied on a session, and a short list of design holes where the current code *doesn't error* but also *doesn't work*. Everything below concerns `2026-07-28` connections only. Every client in the field today negotiates a handshake era, where all of this behaves exactly as it always has.
|
||||
|
|
@ -80,6 +77,8 @@ Separately, the [SDK delegation round two](feature-program.md#sdk-delegation-rou
|
|||
|
||||
The beta-to-stable transition is a small set of tracked steps:
|
||||
|
||||
- **Swap the pins.** When `mcp 2.0.0` reaches GA, change `mcp-types==2.0.0b1` (core) and the `mcp` pin (the `[mcp]` extra) in `fastmcp_slim/pyproject.toml` from the beta to the stable release, and cut `4.0.0` instead of another pre-release.
|
||||
- **Re-run the xfail suite against the GA SDK.** Any strict xfail that starts passing means a gap closed — remove the marker and, where applicable, the corresponding shim.
|
||||
- **Confirm `release/3.x`** is cut from pre-merge `main` and receiving upstream security patches for users who stay on the SDK v1 line.
|
||||
- **Stable SDK dependencies — complete.** `fastmcp-slim` requires `mcp>=2.0.0,<3.0.0` and `mcp-types>=2.0.0,<3.0.0`; the lock resolves both to 2.0.0.
|
||||
- **Re-run the full suite before GA.** Confirm the three expected xfails above remain the complete set. If either strict Apps xfail starts passing, remove the marker and the corresponding compatibility note.
|
||||
- **Make the extension compatibility decision explicit.** GA can accept Apps and other extensions as modern-era capabilities, or wait for the SDK to preserve `capabilities.extensions` on legacy handshakes. Record that choice in the public protocol-support docs.
|
||||
- **Prepare the stable docs.** Remove prerelease installation guidance, add the `4.0.0: Fourmidable` changelog and update entries, and merge those changes to `main` before tagging so the stable docs publication PR contains them.
|
||||
- **Keep the 3.x maintenance line available — complete.** `release/3.x` is protected and continues receiving security and compatibility patches for SDK v1 users.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,76 @@ rss: true
|
|||
tag: NEW
|
||||
---
|
||||
|
||||
<Update label="v4.0.0b3" description="2026-08-14">
|
||||
|
||||
**[v4.0.0b3: Fast Fourward](https://github.com/PrefectHQ/fastmcp/releases/tag/v4.0.0b3)**
|
||||
|
||||
FastMCP 4 beta 3 moves the v4 line toward general availability with Prefect Horizon authentication, `CallArgument` and `Depends` bindings for tools and background tasks, and a round of OAuth, proxy, OpenAPI, and Python 3.14 compatibility hardening.
|
||||
|
||||
### Enhancements ✨
|
||||
* Add Prefect Horizon authentication client and local state by [@parkedwards](https://github.com/parkedwards) in [#4785](https://github.com/PrefectHQ/fastmcp/pull/4785)
|
||||
* Clarify auto-closed PR message by [@jlowin](https://github.com/jlowin) in [#4820](https://github.com/PrefectHQ/fastmcp/pull/4820)
|
||||
* Support CallArgument and Depends bindings from uncalled-for 0.4.0 by [@chrisguidry](https://github.com/chrisguidry) in [#4802](https://github.com/PrefectHQ/fastmcp/pull/4802)
|
||||
* Fix static analysis under newer ty releases by [@zzstoatzz](https://github.com/zzstoatzz) in [#4831](https://github.com/PrefectHQ/fastmcp/pull/4831)
|
||||
* Cover CallArgument resolution in background tasks by [@zzstoatzz](https://github.com/zzstoatzz) in [#4833](https://github.com/PrefectHQ/fastmcp/pull/4833)
|
||||
* Scalekit issuer updates backward compatibility by [@AkshayParihar33](https://github.com/AkshayParihar33) in [#4798](https://github.com/PrefectHQ/fastmcp/pull/4798)
|
||||
|
||||
### Security 🔒
|
||||
* Add audience pinning to GoogleTokenVerifier by [@zzstoatzz](https://github.com/zzstoatzz) in [#4827](https://github.com/PrefectHQ/fastmcp/pull/4827)
|
||||
* Bump cryptography to 50.0.0 by [@zzstoatzz](https://github.com/zzstoatzz) in [#4836](https://github.com/PrefectHQ/fastmcp/pull/4836)
|
||||
|
||||
### Fixes 🐞
|
||||
* Fix partial parameter hints on Python 3.14 by [@zzstoatzz](https://github.com/zzstoatzz) in [#4796](https://github.com/PrefectHQ/fastmcp/pull/4796)
|
||||
* fix(openapi): extract parameter-level example and examples by [@doneman536](https://github.com/doneman536) in [#4793](https://github.com/PrefectHQ/fastmcp/pull/4793)
|
||||
* Keep earlier consent CSRF tokens valid within a transaction by [@trevhud](https://github.com/trevhud) in [#4818](https://github.com/PrefectHQ/fastmcp/pull/4818)
|
||||
* Fix StatefulProxyClient reconnection after session failure by [@jlowin](https://github.com/jlowin) in [#4829](https://github.com/PrefectHQ/fastmcp/pull/4829)
|
||||
|
||||
### Docs 📚
|
||||
* Docs language dropdown by [@znicholasbrown](https://github.com/znicholasbrown) in [#4801](https://github.com/PrefectHQ/fastmcp/pull/4801)
|
||||
* Docs: mirror v3.4.7 release notes by [@jlowin](https://github.com/jlowin) in [#4811](https://github.com/PrefectHQ/fastmcp/pull/4811)
|
||||
* docs: prepare FastMCP 4 beta 3 by [@jlowin](https://github.com/jlowin) in [#4840](https://github.com/PrefectHQ/fastmcp/pull/4840)
|
||||
* docs: add FastMCP 4 beta 3 release entries by [@jlowin](https://github.com/jlowin) in [#4841](https://github.com/PrefectHQ/fastmcp/pull/4841)
|
||||
|
||||
## New Contributors
|
||||
* @parkedwards made their first contribution in [#4785](https://github.com/PrefectHQ/fastmcp/pull/4785)
|
||||
* @trevhud made their first contribution in [#4818](https://github.com/PrefectHQ/fastmcp/pull/4818)
|
||||
|
||||
**Full Changelog**: [v4.0.0b2...v4.0.0b3](https://github.com/PrefectHQ/fastmcp/compare/v4.0.0b2...v4.0.0b3)
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="v3.4.7" description="2026-08-10">
|
||||
|
||||
**[v3.4.7: Know Your Audience](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.7)**
|
||||
|
||||
FastMCP 3.4.7 fixes CIMD `private_key_jwt` authentication on bare-origin OAuth proxy deployments by validating client assertions against the exact token endpoint advertised in OAuth metadata.
|
||||
|
||||
### Security 🔒
|
||||
* Backport CIMD assertion audience fix to v3 by [@jlowin](https://github.com/jlowin) in [#4799](https://github.com/PrefectHQ/fastmcp/pull/4799)
|
||||
|
||||
### Docs 📚
|
||||
* Docs: add v3.4.7 changelog entries by [@jlowin](https://github.com/jlowin) in [#4810](https://github.com/PrefectHQ/fastmcp/pull/4810)
|
||||
|
||||
**Full Changelog**: [v3.4.6...v3.4.7](https://github.com/PrefectHQ/fastmcp/compare/v3.4.6...v3.4.7)
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="v3.4.6" description="2026-08-05">
|
||||
|
||||
**[v3.4.6: Trust, but Proxy](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.6)**
|
||||
|
||||
FastMCP 3.4.6 backports trusted-proxy support for SSRF-protected OAuth metadata and JWKS fetches. Deployments can now route these requests through a mandated corporate proxy while preserving custom CA certificates; FastMCP refuses the fetch when no proxy is configured instead of risking an unprotected direct request.
|
||||
|
||||
### Fixes 🐞
|
||||
* Backport #4412 to 3.x: support trusted SSRF proxies by [@jlowin](https://github.com/jlowin) in [#4755](https://github.com/PrefectHQ/fastmcp/pull/4755)
|
||||
|
||||
### Docs 📚
|
||||
* Docs: add v3.4.6 changelog entries by [@jlowin](https://github.com/jlowin) in [#4761](https://github.com/PrefectHQ/fastmcp/pull/4761)
|
||||
|
||||
**Full Changelog**: [v3.4.5...v3.4.6](https://github.com/PrefectHQ/fastmcp/compare/v3.4.5...v3.4.6)
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="v4.0.0b1" description="2026-07-28">
|
||||
|
||||
**[v4.0.0b1: Fourgone Conclusion](https://github.com/PrefectHQ/fastmcp/releases/tag/v4.0.0b1)**
|
||||
|
|
|
|||
86
docs/css/language-dropdown.css
Normal file
86
docs/css/language-dropdown.css
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/* Language dropdown: injected by language-dropdown.js into the sidebar
|
||||
footer, to the right of Mintlify's theme selector. Mirrors the almond
|
||||
theme pill's exact metrics (lg:h-7 desktop / 2.375rem mobile, rounded-full,
|
||||
border-gray-200/70, dark:border-white/[0.07]) so the two controls read as
|
||||
one family. */
|
||||
#language-switch {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#language-switch select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid rgb(229 231 235 / 0.7);
|
||||
border-radius: 9999px;
|
||||
color: rgb(107 114 128);
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
height: 2.375rem;
|
||||
padding: 0 1.375rem 0 0.75rem;
|
||||
/* Chevron, drawn in the same gray as the label text. */
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.5rem center;
|
||||
background-size: 0.7rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
#language-switch select {
|
||||
height: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
#language-switch select:hover {
|
||||
color: rgb(75 85 99);
|
||||
border-color: rgb(229 231 235);
|
||||
}
|
||||
|
||||
#language-switch select:focus-visible {
|
||||
outline: 2px solid rgb(45 0 247 / 0.4);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.dark #language-switch select {
|
||||
border-color: rgb(255 255 255 / 0.07);
|
||||
color: rgb(156 163 175);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.dark #language-switch select:hover {
|
||||
color: rgb(209 213 219);
|
||||
border-color: rgb(255 255 255 / 0.1);
|
||||
}
|
||||
|
||||
/* Colored language mark on the visible trigger; the native <option>s stay
|
||||
plain. language-dropdown.js mirrors the current selection onto data-lang,
|
||||
so the icon always matches the selected value. Brand colors carry their
|
||||
own contrast (the TS mark keeps a white plate behind the letters), so the
|
||||
same artwork works in both themes. */
|
||||
#language-switch::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0.625rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
background: center / contain no-repeat;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Two-tone Python mark (#3776AB / #FFD43B). */
|
||||
#language-switch[data-lang="python"]::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'%3E%3Cpath fill='%233776AB' d='M63.391 1.988c-4.222.02-8.252.379-11.8 1.007-10.45 1.846-12.346 5.71-12.346 12.837v9.411h24.693v3.137H29.977c-7.176 0-13.46 4.313-15.426 12.521-2.268 9.405-2.368 15.275 0 25.096 1.755 7.311 5.947 12.519 13.124 12.519h8.491V67.234c0-8.151 7.051-15.34 15.426-15.34h24.665c6.866 0 12.346-5.654 12.346-12.548V15.833c0-6.693-5.646-11.72-12.346-12.837-4.244-.706-8.645-1.027-12.866-1.008zM50.037 9.557c2.55 0 4.634 2.117 4.634 4.721 0 2.593-2.083 4.69-4.634 4.69-2.56 0-4.633-2.097-4.633-4.69-.001-2.604 2.073-4.721 4.633-4.721z'/%3E%3Cpath fill='%23FFD43B' d='M91.682 28.38v10.966c0 8.5-7.208 15.655-15.426 15.655H51.591c-6.756 0-12.346 5.783-12.346 12.549v23.515c0 6.691 5.818 10.628 12.346 12.547 7.816 2.297 15.312 2.713 24.665 0 6.216-1.801 12.346-5.423 12.346-12.547v-9.412H63.938v-3.138h37.012c7.176 0 9.852-5.005 12.348-12.519 2.578-7.735 2.467-15.174 0-25.096-1.774-7.145-5.161-12.521-12.348-12.521h-9.268zM77.809 87.927c2.561 0 4.634 2.097 4.634 4.692 0 2.602-2.074 4.719-4.634 4.719-2.55 0-4.633-2.117-4.633-4.719 0-2.595 2.083-4.692 4.633-4.692z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* TypeScript mark: #3178C6 rounded square, white TS (white plate under the
|
||||
letter knockouts keeps them white in dark mode). */
|
||||
#language-switch[data-lang="typescript"]::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect x='2' y='2' width='20' height='20' fill='%23fff'/%3E%3Cpath fill='%233178C6' d='M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75c.612 0 1.154.037 1.627.111a6.38 6.38 0 0 1 1.306.34v2.458a3.95 3.95 0 0 0-.643-.361 5.093 5.093 0 0 0-.717-.26 5.453 5.453 0 0 0-1.426-.2c-.3 0-.573.028-.819.086a2.1 2.1 0 0 0-.623.242c-.17.104-.3.229-.393.374a.888.888 0 0 0-.14.49c0 .196.053.373.156.529.104.156.252.304.443.444s.423.276.696.41c.273.135.582.274.926.416.47.197.892.407 1.266.628.374.222.695.473.963.753.268.279.472.598.614.957.142.359.214.776.214 1.253 0 .657-.125 1.21-.373 1.656a3.033 3.033 0 0 1-1.012 1.085 4.38 4.38 0 0 1-1.487.596c-.566.12-1.163.18-1.79.18a9.916 9.916 0 0 1-1.84-.164 5.544 5.544 0 0 1-1.512-.493v-2.63a5.033 5.033 0 0 0 3.237 1.2c.333 0 .624-.03.872-.09.249-.06.456-.144.623-.25.166-.108.29-.234.373-.38a1.023 1.023 0 0 0-.074-1.089 2.12 2.12 0 0 0-.537-.5 5.597 5.597 0 0 0-.807-.444 27.72 27.72 0 0 0-1.007-.436c-.918-.383-1.602-.852-2.053-1.405-.45-.553-.676-1.222-.676-2.005 0-.614.123-1.141.369-1.582.246-.441.58-.804 1.004-1.089a4.494 4.494 0 0 1 1.47-.629 7.536 7.536 0 0 1 1.77-.201zm-15.113.188h9.563v2.166H9.506v9.646H6.789v-9.646H3.375z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ Our release process is intentionally simple:
|
|||
2. Generate release notes automatically, and curate or add additional editorial information as needed
|
||||
3. GitHub releases automatically trigger PyPI deployments
|
||||
|
||||
Current-major releases target `main`. Maintenance releases target their release branch, such as `release/3.x` for 3.x patches and `release/2.x` for 2.x patches. Stable releases from `main` update the `published-docs` branch after PyPI publishing succeeds; maintenance releases publish packages and GitHub release notes without repointing the live docs branch.
|
||||
Current-major releases target `main`. Maintenance releases target their release branch, such as `release/3.x` for 3.x patches and `release/2.x` for 2.x patches. Stable releases from `main` open a PR that syncs the release commit to `published-docs` after PyPI publishing succeeds; merging that PR publishes the live docs. Prereleases skip the automatic PR and use the same PR-based sync when their docs are ready to publish. Maintenance releases publish packages and GitHub release notes without repointing the live docs branch.
|
||||
|
||||
This automation lets maintainers focus on code quality rather than release mechanics.
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
"dark": "#475569",
|
||||
"light": "#1e3a5f"
|
||||
},
|
||||
"content": "FastMCP 4 is in beta — build stateful applications on sessionless MCP. [See what's new](/getting-started/whats-new)."
|
||||
"content": "FastMCP 4 beta is here — get the latest MCP protocol. [See what's new](/getting-started/whats-new)."
|
||||
},
|
||||
"colors": {
|
||||
"dark": "#f72585",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ pip install fastmcp
|
|||
```
|
||||
|
||||
<Note>
|
||||
**FastMCP 4 is in prerelease.** The commands above install the latest stable release, which is still 3.x. To get v4, pin the beta explicitly with `pip install "fastmcp==4.0.0b1"`, or see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease) for the uv constraint you'll need.
|
||||
**FastMCP 4 is in prerelease.** The commands above install the latest stable release, which is still 3.x. To get v4, pin the beta explicitly with `pip install "fastmcp==4.0.0b3"`, or see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease) for the uv constraint you'll need.
|
||||
</Note>
|
||||
|
||||
### Optional Dependencies
|
||||
|
|
@ -44,7 +44,7 @@ You should see output like the following:
|
|||
```bash
|
||||
$ fastmcp version
|
||||
|
||||
FastMCP version: 4.0.0b1
|
||||
FastMCP version: 4.0.0b3
|
||||
MCP version: 2.0.0
|
||||
Python version: 3.12.2
|
||||
Platform: macOS-15.3.1-arm64-arm-64bit
|
||||
|
|
@ -115,7 +115,7 @@ FastMCP follows semantic versioning with pragmatic adaptations for the rapidly e
|
|||
|
||||
For production use, always pin to exact versions:
|
||||
```
|
||||
fastmcp==4.0.0b1 # Good - an exact version
|
||||
fastmcp==4.0.0b3 # Good - an exact version
|
||||
fastmcp>=4.0.0 # Bad - may install breaking changes
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -16,17 +16,17 @@ The sections below cover what FastMCP handles for you, the changes you must make
|
|||
While FastMCP 4 is in prerelease, pin the beta explicitly. The `fastmcp` package is a thin wrapper that depends on `fastmcp-slim` at the same version, so asking for a prerelease of one means asking for a prerelease of the other. pip infers that on its own:
|
||||
|
||||
```bash
|
||||
pip install "fastmcp==4.0.0b1"
|
||||
pip install "fastmcp==4.0.0b3"
|
||||
```
|
||||
|
||||
uv is stricter: it allows prereleases only for packages you name, and `fastmcp-slim` arrives transitively. Constrain it alongside the requirement in `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[project]
|
||||
dependencies = ["fastmcp==4.0.0b1"]
|
||||
dependencies = ["fastmcp==4.0.0b3"]
|
||||
|
||||
[tool.uv]
|
||||
constraint-dependencies = ["fastmcp-slim==4.0.0b1"]
|
||||
constraint-dependencies = ["fastmcp-slim==4.0.0b3"]
|
||||
```
|
||||
|
||||
Then run `uv lock` or `uv sync` normally. Naming the one package keeps the rest of your graph on stable releases, where `--prerelease allow` would opt every dependency into prereleases. The MCP SDK needs no constraint at all now that it ships stable releases — pinning `mcp==2.0.0b2` here would in fact break the resolution, since a prerelease does not satisfy FastMCP's own `mcp>=2.0.0` requirement.
|
||||
|
|
@ -209,7 +209,7 @@ transport = StreamableHttpTransport(
|
|||
)
|
||||
```
|
||||
|
||||
The `client` you pass to `FastMCP.from_openapi(client=...)` (and `OpenAPIProvider(client=...)`) is now type-hinted `httpx2.AsyncClient`. FastMCP does not gate on the type, so an existing `httpx.AsyncClient` keeps working at runtime via duck-typing this release — but switching it to `httpx2.AsyncClient` clears the type hint and is the supported path going forward. HTTP made inside your own tools is entirely yours and is unaffected either way.
|
||||
The `client` you pass to `FastMCP.from_openapi(client=...)` (and `OpenAPIProvider(client=...)`) should now be an `httpx2.AsyncClient`. Existing `httpx.AsyncClient` instances remain temporarily accepted via duck typing, but emit a `FastMCPDeprecationWarning` and will be rejected in a future release. HTTP made inside your own tools is entirely yours and is unaffected.
|
||||
|
||||
**The subtlest break is exception handlers, and no type checker will catch it.** `httpx` very likely remains installed in your environment (the Anthropic, OpenAI, and Google SDKs all depend on it), so code that catches old-httpx exceptions around FastMCP calls still imports and still type-checks — it just never matches, because FastMCP now raises `httpx2` exceptions. The handler silently becomes dead code:
|
||||
|
||||
|
|
@ -307,14 +307,12 @@ The extension ships in a separate package, so the pin from [Install the v4 Prere
|
|||
|
||||
```toml
|
||||
[project]
|
||||
dependencies = ["fastmcp[tasks]==4.0.0b1"]
|
||||
dependencies = ["fastmcp[tasks]==4.0.0b3"]
|
||||
|
||||
[tool.uv]
|
||||
constraint-dependencies = [
|
||||
"fastmcp-slim==4.0.0b1",
|
||||
"fastmcp-tasks==4.0.0b1",
|
||||
"mcp==2.0.0b2",
|
||||
"mcp-types==2.0.0b2",
|
||||
"fastmcp-slim==4.0.0b3",
|
||||
"fastmcp-tasks==4.0.0b3",
|
||||
]
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -81,15 +81,13 @@ For each item found, show the original code, say what it did, and give the FastM
|
|||
|
||||
## Install
|
||||
|
||||
FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` or `uv add fastmcp` resolves to the latest *stable* release, which today is FastMCP 3:
|
||||
FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` resolves to the latest *stable* release, which today is FastMCP 3:
|
||||
|
||||
```bash
|
||||
pip install "fastmcp==4.0.0b1"
|
||||
# or
|
||||
uv add "fastmcp==4.0.0b1"
|
||||
pip install "fastmcp==4.0.0b3"
|
||||
```
|
||||
|
||||
An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
|
||||
An exact pip pin installs even though it's a prerelease; it does not need `--pre`. uv also needs an explicit constraint for the transitive `fastmcp-slim` prerelease, so follow [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease) for a reproducible uv setup.
|
||||
|
||||
FastMCP depends on the `mcp` package, so the SDK stays installed. FastMCP 4 builds on SDK v2, where the protocol types live in a standalone `mcp_types` package that stays importable as `mcp.types`. Most of your `mcp.types` imports disappear entirely in the rewrite below, since FastMCP derives the protocol types from your function signatures.
|
||||
|
||||
|
|
|
|||
|
|
@ -66,15 +66,13 @@ For each item found, show the original code, say what it did, and give the FastM
|
|||
|
||||
## Install
|
||||
|
||||
FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` or `uv add fastmcp` resolves to the latest *stable* release, which today is FastMCP 3:
|
||||
FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` resolves to the latest *stable* release, which today is FastMCP 3:
|
||||
|
||||
```bash
|
||||
pip install "fastmcp==4.0.0b1"
|
||||
# or
|
||||
uv add "fastmcp==4.0.0b1"
|
||||
pip install "fastmcp==4.0.0b3"
|
||||
```
|
||||
|
||||
An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
|
||||
An exact pip pin installs even though it's a prerelease; it does not need `--pre`. uv also needs an explicit constraint for the transitive `fastmcp-slim` prerelease, so follow [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease) for a reproducible uv setup.
|
||||
|
||||
FastMCP 4 depends on the MCP SDK v2 you are already using, so `mcp_types` stays importable and every protocol type keeps its current name and fields. Most of those imports vanish from your code anyway — FastMCP derives them — but the ones you keep need no changes.
|
||||
|
||||
|
|
|
|||
|
|
@ -51,15 +51,13 @@ If you have already moved to SDK v2 and write against `MCPServer` today, see [Up
|
|||
|
||||
## Install
|
||||
|
||||
FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` or `uv add fastmcp` resolves to the latest *stable* release, which today is FastMCP 3:
|
||||
FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` resolves to the latest *stable* release, which today is FastMCP 3:
|
||||
|
||||
```bash
|
||||
pip install "fastmcp==4.0.0b1"
|
||||
# or
|
||||
uv add "fastmcp==4.0.0b1"
|
||||
pip install "fastmcp==4.0.0b3"
|
||||
```
|
||||
|
||||
An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
|
||||
An exact pip pin installs even though it's a prerelease; it does not need `--pre`. uv also needs an explicit constraint for the transitive `fastmcp-slim` prerelease, so follow [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease) for a reproducible uv setup.
|
||||
|
||||
FastMCP depends on the `mcp` package, so the SDK stays installed and importable. What changes is which parts of it you reach for. FastMCP 4 builds on SDK v2, where `mcp.server.fastmcp` is gone — anything you imported from it needs a new home, and the sections below cover that. `mcp.types` still resolves (it aliases the standalone `mcp_types` package), though its fields are snake_case now. Update your import, run your server, and if your tools work, you're done.
|
||||
|
||||
|
|
|
|||
|
|
@ -91,15 +91,13 @@ For each item found, show the original code, name what changed, and give the Fas
|
|||
|
||||
## Install
|
||||
|
||||
FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` or `uv add fastmcp` resolves to the latest *stable* release, which today is FastMCP 3:
|
||||
FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` resolves to the latest *stable* release, which today is FastMCP 3:
|
||||
|
||||
```bash
|
||||
pip install "fastmcp==4.0.0b1"
|
||||
# or
|
||||
uv add "fastmcp==4.0.0b1"
|
||||
pip install "fastmcp==4.0.0b3"
|
||||
```
|
||||
|
||||
An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
|
||||
An exact pip pin installs even though it's a prerelease; it does not need `--pre`. uv also needs an explicit constraint for the transitive `fastmcp-slim` prerelease, so follow [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease) for a reproducible uv setup.
|
||||
|
||||
FastMCP 4 depends on the MCP SDK v2, so nothing you already import from `mcp_types` moves. That is the practical benefit of migrating at this version rather than an earlier one: you and FastMCP are on the same protocol layer, with the same snake_case field names and the same type package, so the migration touches only the server API.
|
||||
|
||||
|
|
|
|||
77
docs/language-dropdown.js
Normal file
77
docs/language-dropdown.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Language dropdown: a small Python/TypeScript switcher injected into the
|
||||
// sidebar footer, next to Mintlify's theme selector. Selecting the other
|
||||
// language navigates to that project's docs site; selecting the current
|
||||
// language is a no-op. Styling lives in css/language-dropdown.css.
|
||||
(function () {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
var CURRENT_LANGUAGE = "python";
|
||||
|
||||
var TYPESCRIPT_DOCS_URL = "https://fastmcp-ts.docs.prefect.io/";
|
||||
var PYTHON_DOCS_URL = "https://gofastmcp.com";
|
||||
|
||||
var URLS = { python: PYTHON_DOCS_URL, typescript: TYPESCRIPT_DOCS_URL };
|
||||
|
||||
function findThemeSelector() {
|
||||
// Mintlify's sidebar-footer DOM is not a stable public API, so probe a
|
||||
// few markers (almond theme first) and give up quietly if none match.
|
||||
return (
|
||||
document.querySelector("[data-theme-preference-switch]") ||
|
||||
document.querySelector('[role="group"][aria-label="Theme preference"]')
|
||||
);
|
||||
}
|
||||
|
||||
function buildDropdown() {
|
||||
var label = document.createElement("label");
|
||||
label.id = "language-switch";
|
||||
// The CSS keys the trigger's language icon off this attribute.
|
||||
label.dataset.lang = CURRENT_LANGUAGE;
|
||||
|
||||
var select = document.createElement("select");
|
||||
select.setAttribute("aria-label", "Switch documentation language");
|
||||
|
||||
[
|
||||
["python", "Python"],
|
||||
["typescript", "TypeScript"],
|
||||
].forEach(function (entry) {
|
||||
var option = document.createElement("option");
|
||||
option.value = entry[0];
|
||||
option.textContent = entry[1];
|
||||
if (entry[0] === CURRENT_LANGUAGE) option.selected = true;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
select.addEventListener("change", function () {
|
||||
label.dataset.lang = select.value;
|
||||
if (select.value === CURRENT_LANGUAGE) return;
|
||||
window.location.href = URLS[select.value];
|
||||
});
|
||||
|
||||
label.appendChild(select);
|
||||
return label;
|
||||
}
|
||||
|
||||
function addDropdown() {
|
||||
if (document.getElementById("language-switch")) return;
|
||||
var theme = findThemeSelector();
|
||||
if (!theme || !theme.parentElement) return;
|
||||
// Insert after the theme pill; margin-left:auto floats it right.
|
||||
theme.parentElement.insertBefore(buildDropdown(), theme.nextSibling);
|
||||
}
|
||||
|
||||
function run() {
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", addDropdown);
|
||||
} else {
|
||||
addDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
// Mintlify re-renders the sidebar on client-side navigation; re-inject when
|
||||
// the dropdown disappears.
|
||||
new MutationObserver(function () {
|
||||
if (!document.getElementById("language-switch")) addDropdown();
|
||||
}).observe(document.body, { subtree: true, childList: true });
|
||||
})();
|
||||
|
|
@ -22,16 +22,24 @@ The client probes `server/discover` and adopts the modern protocol when the serv
|
|||
|
||||
## What are the two protocol eras, and which one does my server speak?
|
||||
|
||||
Both. A FastMCP server serves every era from one deployment and one URL, and the SDK negotiates per connection — the client picks, not the server.
|
||||
Both. A FastMCP 4 server supports the handshake revisions `2024-11-05`, `2025-03-26`, `2025-06-18`, and `2025-11-25`, plus the modern `2026-07-28` protocol. It serves all of them from one deployment and one URL, and the SDK negotiates per connection — the client picks, not the server.
|
||||
|
||||
The *handshake* era (`2025-11-25` and earlier) opens each connection with `initialize` and holds a session, which gives the server a back-channel it can push requests down. The *modern* era (`2026-07-28`) is sessionless: the client learns what the server offers through `server/discover`, every request stands alone, and there is no back-channel. Inside a tool, `ctx.request_context.protocol_version` tells you which era the current call arrived on; on the client, `client.protocol_version` reports it after connecting.
|
||||
|
||||
A protocol version establishes the wire format, while capabilities describe which optional operations a particular server provides. The capabilities returned by `server/discover` or `initialize` are therefore the authoritative way for a client to determine what is available.
|
||||
|
||||
## Can FastMCP 4 talk to older clients and servers?
|
||||
|
||||
Yes, in both directions, with no configuration. A FastMCP 4 server answers a handshake-era client and a modern one from the same process: the old client sends `initialize` and gets a session id, the modern client discovers and stays stateless.
|
||||
|
||||
A FastMCP 4 client is equally happy against an old server, because `mode="auto"` falls back to the handshake when discovery finds no modern peer. The client-side handlers for server-initiated capabilities are all still there too — passing `sampling_handler=` or `roots=` answers a legacy server's requests exactly as before, which is what a modern client needs in order to interoperate. See [client sampling](/clients/sampling) and [client roots](/clients/roots).
|
||||
|
||||
## How does FastMCP verify protocol conformance?
|
||||
|
||||
FastMCP runs the [official MCP conformance suite](https://github.com/modelcontextprotocol/conformance) in CI against a pinned suite release. A failing scenario for a released capability that FastMCP advertises as supported is treated as a regression.
|
||||
|
||||
The suite's `all` mode also exercises draft, pending, retired, and deliberately unsupported capabilities, so its raw pass count is broader than FastMCP's support contract. Known exceptions are recorded in [`expected-failures.yml`](https://github.com/PrefectHQ/fastmcp/blob/main/tests/conformance/expected-failures.yml) with their rationale, and new upstream scenarios arrive through deliberate suite-version updates rather than silently changing CI.
|
||||
|
||||
## When should I pin `mode="legacy"`?
|
||||
|
||||
Pin it when your code depends on the session the handshake creates: `client.ping()` and `transport.get_session_id()` have no modern equivalent, since a sessionless connection has neither a live back-channel to ping nor an id to hold. It is also the escape hatch when a server misbehaves under discovery or you need the classic `initialize` result object.
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ These control how the server listens when running with an HTTP transport.
|
|||
|
||||
## Tasks (Docket)
|
||||
|
||||
Task settings (the `FASTMCP_DOCKET_` variables) moved to the optional `fastmcp-tasks` package. See [server tasks](/servers/tasks) for configuration.
|
||||
Task settings (the `FASTMCP_DOCKET_` and `FASTMCP_TASKS_` variables) live in the optional `fastmcp-tasks` package. See [server tasks](/servers/tasks) for configuration, including `FASTMCP_TASKS_ENCRYPTION_KEY` for [encrypting task snapshots at rest](/servers/tasks#credentials-at-rest).
|
||||
|
||||
## Security
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,38 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "fastmcp.resources",
|
||||
"pages": [
|
||||
"python-sdk/fastmcp-resources-base",
|
||||
"python-sdk/fastmcp-resources-function_resource",
|
||||
"python-sdk/fastmcp-resources-security",
|
||||
"python-sdk/fastmcp-resources-template",
|
||||
"python-sdk/fastmcp-resources-types"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "fastmcp.server",
|
||||
"pages": [
|
||||
"python-sdk/fastmcp-server-caching",
|
||||
"python-sdk/fastmcp-server-completions",
|
||||
"python-sdk/fastmcp-server-context",
|
||||
"python-sdk/fastmcp-server-dependencies",
|
||||
"python-sdk/fastmcp-server-elicitation",
|
||||
"python-sdk/fastmcp-server-event_store",
|
||||
"python-sdk/fastmcp-server-extensions",
|
||||
"python-sdk/fastmcp-server-http",
|
||||
"python-sdk/fastmcp-server-lifespan",
|
||||
"python-sdk/fastmcp-server-low_level",
|
||||
"python-sdk/fastmcp-server-mixins",
|
||||
"python-sdk/fastmcp-server-providers",
|
||||
"python-sdk/fastmcp-server-server",
|
||||
"python-sdk/fastmcp-server-session_scoped_event_store",
|
||||
"python-sdk/fastmcp-server-sessions",
|
||||
"python-sdk/fastmcp-server-telemetry",
|
||||
"python-sdk/fastmcp-server-transforms"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "fastmcp.utilities",
|
||||
"pages": [
|
||||
|
|
@ -79,6 +111,7 @@
|
|||
"python-sdk/fastmcp-utilities-mime",
|
||||
"python-sdk/fastmcp-utilities-openapi",
|
||||
"python-sdk/fastmcp-utilities-pagination",
|
||||
"python-sdk/fastmcp-utilities-prefab",
|
||||
"python-sdk/fastmcp-utilities-skills",
|
||||
"python-sdk/fastmcp-utilities-tasks",
|
||||
"python-sdk/fastmcp-utilities-tests",
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ Usage::
|
|||
|
||||
## Classes
|
||||
|
||||
### `FastMCPApp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L145" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `FastMCPApp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L149" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A Provider that represents an MCP application.
|
||||
|
|
@ -48,19 +48,19 @@ can find them by original name even when transforms have been applied.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L169" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L173" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
tool(self, name_or_fn: F) -> F
|
||||
```
|
||||
|
||||
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L181" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L185" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
tool(self, name_or_fn: str | None = None) -> Callable[[F], F]
|
||||
```
|
||||
|
||||
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L192" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L196" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
tool(self, name_or_fn: str | AnyFunction | None = None) -> Any
|
||||
|
|
@ -83,19 +83,19 @@ Supports multiple calling patterns::
|
|||
def save(name: str): ...
|
||||
|
||||
|
||||
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L259" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L266" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
ui(self, name_or_fn: F) -> F
|
||||
```
|
||||
|
||||
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L274" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L281" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
ui(self, name_or_fn: str | None = None) -> Callable[[F], F]
|
||||
```
|
||||
|
||||
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L288" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L295" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
ui(self, name_or_fn: str | AnyFunction | None = None) -> Any
|
||||
|
|
@ -119,7 +119,7 @@ Supports multiple calling patterns::
|
|||
def dashboard() -> Component: ...
|
||||
|
||||
|
||||
#### `add_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L363" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `add_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L373" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_tool(self, tool: Tool | Callable[..., Any]) -> Tool
|
||||
|
|
@ -130,13 +130,13 @@ Add a tool to this app programmatically.
|
|||
The tool is tagged with this app's name for routing.
|
||||
|
||||
|
||||
#### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L419" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L432" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
lifespan(self) -> AsyncIterator[None]
|
||||
```
|
||||
|
||||
#### `run` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L427" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `run` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L440" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
run(self, transport: Literal['stdio', 'http', 'sse', 'streamable-http'] | None = None, **kwargs: Any) -> None
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ UI metadata for clients that support interactive app rendering.
|
|||
|
||||
## Functions
|
||||
|
||||
### `app_config_to_meta_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L180" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `app_config_to_meta_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L181" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
app_config_to_meta_dict(app: AppConfig | dict[str, Any]) -> dict[str, Any]
|
||||
|
|
@ -25,9 +25,32 @@ app_config_to_meta_dict(app: AppConfig | dict[str, Any]) -> dict[str, Any]
|
|||
Convert an AppConfig or dict to the wire-format dict for ``meta["ui"]``.
|
||||
|
||||
|
||||
### `is_model_visible` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L188" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
is_model_visible(component: FastMCPComponent) -> bool
|
||||
```
|
||||
|
||||
|
||||
Whether a component may be shown to, or invoked by, the model.
|
||||
|
||||
Visibility is a declaration, and the MCP Apps spec puts the filtering on
|
||||
the host — so ``tools/list`` carries app-only tools and the host keeps
|
||||
them from the model. That division only works where a host stands between
|
||||
the server and the model.
|
||||
|
||||
It does not hold for surfaces a server drives itself. A search result or
|
||||
a code-mode catalog reaches the model as ordinary tool output, and a
|
||||
call-tool proxy invokes on a name the model supplies; nothing downstream
|
||||
can filter either. Those surfaces have to apply the declaration here.
|
||||
|
||||
A component with no ``visibility`` is visible: the field marks the
|
||||
exception, and the spec's default is both audiences.
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `ResourceCSP` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L20" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ResourceCSP` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L21" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Content Security Policy for MCP App resources.
|
||||
|
|
@ -37,7 +60,7 @@ load resources from. Hosts use these declarations to build the
|
|||
``Content-Security-Policy`` header for the sandboxed iframe.
|
||||
|
||||
|
||||
### `ResourcePermissions` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L56" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ResourcePermissions` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L57" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Iframe sandbox permissions for MCP App resources.
|
||||
|
|
@ -48,7 +71,7 @@ iframe. Hosts MAY honour these; apps should use JS feature detection
|
|||
as a fallback.
|
||||
|
||||
|
||||
### `AppConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L84" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `AppConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L85" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Configuration for MCP App tools and resources.
|
||||
|
|
@ -63,7 +86,7 @@ values appear on the wire. Aliases match the MCP Apps wire format
|
|||
(camelCase).
|
||||
|
||||
|
||||
### `PrefabAppConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L124" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `PrefabAppConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L125" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
App configuration for Prefab tools with sensible defaults.
|
||||
|
|
@ -83,7 +106,7 @@ Example::
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `model_post_init` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L140" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `model_post_init` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L141" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
model_post_init(self, __context: Any) -> None
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Custom exceptions for FastMCP.
|
|||
|
||||
## Functions
|
||||
|
||||
### `to_mcp_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L122" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `to_mcp_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L117" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
to_mcp_error(exc: Exception) -> MCPError
|
||||
|
|
@ -38,71 +38,61 @@ explicit code chosen upstream survives translation.
|
|||
|
||||
## Classes
|
||||
|
||||
### `FastMCPDeprecationWarning` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Deprecation warning for FastMCP APIs.
|
||||
|
||||
Subclass of DeprecationWarning so that standard warning filters
|
||||
still apply, but FastMCP can selectively enable its own warnings
|
||||
without affecting other libraries in the process.
|
||||
|
||||
|
||||
### `FastMCPError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L43" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `FastMCPError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L38" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Base error for FastMCP.
|
||||
|
||||
|
||||
### `ValidationError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L51" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ValidationError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L46" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Error in validating parameters or return values.
|
||||
|
||||
|
||||
### `ResourceError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L55" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ResourceError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L50" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Error in resource operations.
|
||||
|
||||
|
||||
### `ToolError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L59" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ToolError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Error in tool operations.
|
||||
|
||||
|
||||
### `PromptError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `PromptError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L58" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Error in prompt operations.
|
||||
|
||||
|
||||
### `InvalidSignature` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `InvalidSignature` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L62" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Invalid signature for use with FastMCP.
|
||||
|
||||
|
||||
### `ClientError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L71" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ClientError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L66" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Error in client operations.
|
||||
|
||||
|
||||
### `NotFoundError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L75" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `NotFoundError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L70" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Object not found.
|
||||
|
||||
|
||||
### `DisabledError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L79" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `DisabledError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L74" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Object is disabled.
|
||||
|
||||
|
||||
### `ResourceSecurityError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L83" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ResourceSecurityError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L78" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A templated resource parameter failed path-security screening.
|
||||
|
|
@ -114,13 +104,13 @@ for a resource that does not exist, and never reveals which parameter
|
|||
or policy tripped.
|
||||
|
||||
|
||||
### `AuthorizationError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L94" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `AuthorizationError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L89" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Error when authorization check fails.
|
||||
|
||||
|
||||
### `InsufficientScopeError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L98" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `InsufficientScopeError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L93" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Authorization failed because the token is missing required OAuth scopes.
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ Example configuration:
|
|||
|
||||
## Functions
|
||||
|
||||
### `infer_transport_type_from_url` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `infer_transport_type_from_url` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L55" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
infer_transport_type_from_url(url: str | AnyUrl) -> Literal['http', 'sse']
|
||||
|
|
@ -42,7 +42,7 @@ infer_transport_type_from_url(url: str | AnyUrl) -> Literal['http', 'sse']
|
|||
Infer the appropriate transport type from the given URL.
|
||||
|
||||
|
||||
### `update_config_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L373" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `update_config_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L376" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
update_config_file(file_path: Path, server_name: str, server_config: CanonicalMCPServerTypes) -> None
|
||||
|
|
@ -57,7 +57,7 @@ worry about transforming server objects here.
|
|||
|
||||
## Classes
|
||||
|
||||
### `StdioMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L179" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `StdioMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L180" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
MCP server configuration for stdio transport.
|
||||
|
|
@ -67,19 +67,19 @@ This is the canonical configuration format for MCP servers using stdio transport
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `to_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L212" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `to_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L213" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
to_transport(self) -> StdioTransport
|
||||
to_transport(self) -> StdioTransport | FastMCPTransport
|
||||
```
|
||||
|
||||
### `TransformingStdioMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L224" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `TransformingStdioMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L225" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A Stdio server with tool transforms.
|
||||
|
||||
|
||||
### `RemoteMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L228" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `RemoteMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L229" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
MCP server configuration for HTTP/SSE transport.
|
||||
|
|
@ -89,19 +89,19 @@ This is the canonical configuration format for MCP servers using remote transpor
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `to_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L264" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `to_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L265" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
to_transport(self) -> StreamableHttpTransport | SSETransport
|
||||
to_transport(self) -> StreamableHttpTransport | SSETransport | FastMCPTransport
|
||||
```
|
||||
|
||||
### `TransformingRemoteMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L291" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `TransformingRemoteMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L294" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A Remote server with tool transforms.
|
||||
|
||||
|
||||
### `MCPConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L302" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `MCPConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L305" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A configuration object for MCP Servers that conforms to the canonical MCP configuration format
|
||||
|
|
@ -113,7 +113,7 @@ For an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `wrap_servers_at_root` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L316" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `wrap_servers_at_root` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L319" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any]
|
||||
|
|
@ -122,7 +122,7 @@ wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any]
|
|||
If there's no mcpServers key but there are server configs at root, wrap them.
|
||||
|
||||
|
||||
#### `add_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L329" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `add_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L332" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_server(self, name: str, server: MCPServerTypes) -> None
|
||||
|
|
@ -131,7 +131,7 @@ add_server(self, name: str, server: MCPServerTypes) -> None
|
|||
Add or update a server in the configuration.
|
||||
|
||||
|
||||
#### `from_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L334" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `from_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L337" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
from_dict(cls, config: dict[str, Any]) -> Self
|
||||
|
|
@ -140,7 +140,7 @@ from_dict(cls, config: dict[str, Any]) -> Self
|
|||
Parse MCP configuration from dictionary format.
|
||||
|
||||
|
||||
#### `to_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L338" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `to_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L341" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
to_dict(self) -> dict[str, Any]
|
||||
|
|
@ -149,7 +149,7 @@ to_dict(self) -> dict[str, Any]
|
|||
Convert MCPConfig to dictionary format, preserving all fields.
|
||||
|
||||
|
||||
#### `write_to_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L342" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `write_to_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L345" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
write_to_file(self, file_path: Path) -> None
|
||||
|
|
@ -158,7 +158,7 @@ write_to_file(self, file_path: Path) -> None
|
|||
Write configuration to JSON file.
|
||||
|
||||
|
||||
#### `from_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L348" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `from_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L351" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
from_file(cls, file_path: Path) -> Self
|
||||
|
|
@ -167,7 +167,7 @@ from_file(cls, file_path: Path) -> Self
|
|||
Load configuration from JSON file.
|
||||
|
||||
|
||||
### `CanonicalMCPConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L358" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `CanonicalMCPConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L361" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Canonical MCP configuration format.
|
||||
|
|
@ -178,7 +178,7 @@ The format is designed to be client-agnostic and extensible for future use cases
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `add_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L368" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `add_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L371" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_server(self, name: str, server: CanonicalMCPServerTypes) -> None
|
||||
|
|
|
|||
196
docs/python-sdk/fastmcp-resources-base.mdx
Normal file
196
docs/python-sdk/fastmcp-resources-base.mdx
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
---
|
||||
title: base
|
||||
sidebarTitle: base
|
||||
---
|
||||
|
||||
# `fastmcp.resources.base`
|
||||
|
||||
|
||||
Base classes and interfaces for FastMCP resources.
|
||||
|
||||
## Functions
|
||||
|
||||
### `convert_raw_to_resource_result` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/base.py#L270" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
convert_raw_to_resource_result(raw_value: Any) -> ResourceResult
|
||||
```
|
||||
|
||||
|
||||
Wrap a user function's return value in a ResourceResult.
|
||||
|
||||
Shared by `Resource` and `ResourceTemplate` so both honor the MIME type
|
||||
the component declares in listings. A component that advertises
|
||||
`text/csv` must not serve `text/plain` on read.
|
||||
|
||||
**Args:**
|
||||
- `raw_value`: The value returned by the user's function.
|
||||
- `mime_type`: The component's declared MIME type, forwarded to content items.
|
||||
- `meta`: Component-level meta (e.g. `ui` metadata for MCP Apps CSP/permissions)
|
||||
propagated to each content item.
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `ResourceContent` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/base.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Wrapper for resource content with optional MIME type and metadata.
|
||||
|
||||
Accepts any value for content - strings and bytes pass through directly,
|
||||
other types (dict, list, BaseModel, etc.) are automatically JSON-serialized.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `to_mcp_resource_contents` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/base.py#L89" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
to_mcp_resource_contents(self, uri: AnyUrl | str) -> mcp_types.TextResourceContents | mcp_types.BlobResourceContents
|
||||
```
|
||||
|
||||
Convert to MCP resource contents type.
|
||||
|
||||
**Args:**
|
||||
- `uri`: The URI of the resource (required by MCP types)
|
||||
|
||||
**Returns:**
|
||||
- TextResourceContents for str content, BlobResourceContents for bytes
|
||||
|
||||
|
||||
### `ResourceResult` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/base.py#L116" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Canonical result type for resource reads.
|
||||
|
||||
Provides explicit control over resource responses: multiple content items,
|
||||
per-item MIME types, and metadata at both the item and result level.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `to_mcp_result` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/base.py#L197" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
to_mcp_result(self, uri: AnyUrl | str) -> mcp_types.ReadResourceResult
|
||||
```
|
||||
|
||||
Convert to MCP ReadResourceResult.
|
||||
|
||||
**Args:**
|
||||
- `uri`: The URI of the resource (required by MCP types)
|
||||
|
||||
**Returns:**
|
||||
- MCP ReadResourceResult with converted contents
|
||||
|
||||
|
||||
### `InputRequiredResourceResult` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/base.py#L213" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
The full result of a single multi-round-trip resource read (SEP-2322).
|
||||
|
||||
`InputRequiredResult` is a result type, not a `tools/call` feature: any
|
||||
request may resolve to one. When a resource or resource template returns an
|
||||
`InputRequiredResult` from its body to ask the client for input, that ask is
|
||||
the legitimate result of this `resources/read` — so FastMCP wraps it in this
|
||||
`ResourceResult` subclass, mirroring `InputRequiredToolResult` and
|
||||
`InputRequiredPromptResult`, and it flows through the middleware chain as an
|
||||
ordinary return value.
|
||||
|
||||
Invariant: the wrapped `InputRequiredResult` is never serialized as resource
|
||||
contents. `contents` is always empty; the wire handler (`_on_read_resource`)
|
||||
reads `.input_required` and returns it to the runner.
|
||||
|
||||
|
||||
### `Resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/base.py#L334" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Base class for all resources.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `from_function` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/base.py#L359" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
from_function(cls, fn: Callable[..., Any], uri: str | AnyUrl) -> FunctionResource
|
||||
```
|
||||
|
||||
#### `set_default_mime_type` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/base.py#L396" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_default_mime_type(cls, mime_type: str | None) -> str
|
||||
```
|
||||
|
||||
Set default MIME type if not provided.
|
||||
|
||||
|
||||
#### `set_default_name` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/base.py#L403" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_default_name(self) -> Self
|
||||
```
|
||||
|
||||
Set default name from URI if not provided.
|
||||
|
||||
|
||||
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/base.py#L413" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
read(self) -> str | bytes | ResourceResult
|
||||
```
|
||||
|
||||
Read the resource content.
|
||||
|
||||
Subclasses implement this to return resource data. Supported return types:
|
||||
- str: Text content
|
||||
- bytes: Binary content
|
||||
- ResourceResult: Full control over contents and result-level meta
|
||||
|
||||
|
||||
#### `convert_result` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/base.py#L425" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
convert_result(self, raw_value: Any) -> ResourceResult
|
||||
```
|
||||
|
||||
Convert a raw result to ResourceResult.
|
||||
|
||||
This is used in two contexts:
|
||||
1. In _read() to convert user function return values to ResourceResult
|
||||
2. In tasks_result_handler() to convert Docket task results to ResourceResult
|
||||
|
||||
Handles ResourceResult passthrough and converts raw values using
|
||||
ResourceResult's normalization. When the raw value is a plain
|
||||
string or bytes, the resource's own ``mime_type`` is forwarded so
|
||||
that ``ui://`` resources (and others with non-default MIME types)
|
||||
don't fall back to ``text/plain``.
|
||||
|
||||
The resource's component-level ``meta`` (e.g. ``ui`` metadata for
|
||||
MCP Apps CSP/permissions) is propagated to each content item so
|
||||
that hosts can read it from the ``resources/read`` response.
|
||||
|
||||
|
||||
#### `to_mcp_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/base.py#L457" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
to_mcp_resource(self, **overrides: Any) -> SDKResource
|
||||
```
|
||||
|
||||
Convert the resource to an SDKResource.
|
||||
|
||||
|
||||
#### `key` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/base.py#L480" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
key(self) -> str
|
||||
```
|
||||
|
||||
The globally unique lookup key for this resource.
|
||||
|
||||
|
||||
#### `get_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/base.py#L485" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_span_attributes(self) -> dict[str, Any]
|
||||
```
|
||||
81
docs/python-sdk/fastmcp-resources-function_resource.mdx
Normal file
81
docs/python-sdk/fastmcp-resources-function_resource.mdx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
title: function_resource
|
||||
sidebarTitle: function_resource
|
||||
---
|
||||
|
||||
# `fastmcp.resources.function_resource`
|
||||
|
||||
|
||||
Standalone @resource decorator for FastMCP.
|
||||
|
||||
## Functions
|
||||
|
||||
### `resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/function_resource.py#L223" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
resource(uri: str) -> Callable[[F], F]
|
||||
```
|
||||
|
||||
|
||||
Standalone decorator to mark a function as an MCP resource.
|
||||
|
||||
Returns the original function with metadata attached. Register with a server
|
||||
using mcp.add_resource().
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `DecoratedResource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/function_resource.py#L40" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Protocol for functions decorated with @resource.
|
||||
|
||||
|
||||
### `ResourceMeta` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/function_resource.py#L49" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Metadata attached to functions by the @resource decorator.
|
||||
|
||||
|
||||
### `FunctionResource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/function_resource.py#L68" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A resource that defers data loading by wrapping a function.
|
||||
|
||||
The function is only called when the resource is read, allowing for lazy loading
|
||||
of potentially expensive data. This is particularly useful when listing resources,
|
||||
as the function won't be called until the resource is actually accessed.
|
||||
|
||||
The function can return:
|
||||
- str for text content (default)
|
||||
- bytes for binary content
|
||||
- other types will be converted to JSON
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `from_function` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/function_resource.py#L84" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
from_function(cls, fn: Callable[..., Any], uri: str | AnyUrl | None = None) -> FunctionResource
|
||||
```
|
||||
|
||||
Create a FunctionResource from a function.
|
||||
|
||||
**Args:**
|
||||
- `fn`: The function to wrap
|
||||
- `uri`: The URI for the resource (required if metadata not provided)
|
||||
- `metadata`: ResourceMeta object with all configuration. If provided,
|
||||
individual parameters must not be passed.
|
||||
- `name, title, etc.`: Individual parameters for backwards compatibility.
|
||||
Cannot be used together with metadata parameter.
|
||||
|
||||
|
||||
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/function_resource.py#L201" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
read(self) -> str | bytes | ResourceResult
|
||||
```
|
||||
|
||||
Read the resource by calling the wrapped function.
|
||||
|
||||
74
docs/python-sdk/fastmcp-resources-security.mdx
Normal file
74
docs/python-sdk/fastmcp-resources-security.mdx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
---
|
||||
title: security
|
||||
sidebarTitle: security
|
||||
---
|
||||
|
||||
# `fastmcp.resources.security`
|
||||
|
||||
|
||||
Path-safety policy for templated resource parameters.
|
||||
|
||||
Templated resources (`@mcp.resource("file:///{path}")`-style) extract
|
||||
parameter values straight out of the request URI and hand them to the
|
||||
resource function. When those values flow into filesystem or URI
|
||||
construction, a malicious client can smuggle path-traversal payloads
|
||||
(`../`, absolute paths, null bytes) through the template.
|
||||
|
||||
`ResourceSecurity` screens extracted parameter values *before* the
|
||||
resource handler runs. It is applied by default to every templated
|
||||
read, mirroring the posture of the underlying MCP SDK's
|
||||
`ResourceSecurity` (traversal, absolute paths, and null bytes rejected).
|
||||
|
||||
The screening reuses the SDK's component-based traversal check, so a
|
||||
value that merely *contains* dots (e.g. `HEAD~3..HEAD`, `v1..v2`,
|
||||
`file.tar.gz`) is not rejected — only an actual `..` path segment is.
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `InheritSecurity` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/security.py#L49" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Sentinel type: inherit the server-wide resource-security default.
|
||||
|
||||
Distinguishes "no per-component policy was set" (inherit whatever the
|
||||
server configured) from an explicit ``None`` (screening disabled for
|
||||
this component).
|
||||
|
||||
|
||||
### `ResourceSecurity` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/security.py#L76" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Security policy applied to extracted resource template parameters.
|
||||
|
||||
These checks run after a URI has matched a template and its
|
||||
parameter values have been extracted and percent-decoded. They catch
|
||||
path-traversal and absolute-path injection regardless of how the
|
||||
value was encoded in the URI (literal, `%2F`, `%5C`, `%2E%2E`).
|
||||
|
||||
All checks default on. Screen a value like `HEAD~3..HEAD` (dots
|
||||
inside a single segment) passes — only a standalone `..` segment is
|
||||
treated as traversal.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `validate` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/security.py#L127" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
validate(self, params: Mapping[str, object]) -> str | None
|
||||
```
|
||||
|
||||
Check all parameter values against the configured policy.
|
||||
|
||||
String values (and lists of strings, from wildcard `{path*}`
|
||||
parameters that span multiple segments) are screened; non-string
|
||||
values are ignored, since traversal is a string-path concern.
|
||||
|
||||
**Args:**
|
||||
- `params`: Extracted template parameters.
|
||||
|
||||
**Returns:**
|
||||
- The name of the first parameter that fails, or `None` if all
|
||||
- values pass.
|
||||
|
||||
224
docs/python-sdk/fastmcp-resources-template.mdx
Normal file
224
docs/python-sdk/fastmcp-resources-template.mdx
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
---
|
||||
title: template
|
||||
sidebarTitle: template
|
||||
---
|
||||
|
||||
# `fastmcp.resources.template`
|
||||
|
||||
|
||||
Resource template functionality.
|
||||
|
||||
## Functions
|
||||
|
||||
### `extract_query_params` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L38" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
extract_query_params(uri_template: str) -> set[str]
|
||||
```
|
||||
|
||||
|
||||
Extract query parameter names from RFC 6570 `{?param1,param2}` syntax.
|
||||
|
||||
|
||||
### `build_regex` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L46" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
build_regex(template: str) -> re.Pattern[str] | None
|
||||
```
|
||||
|
||||
|
||||
Build regex pattern for URI template, handling RFC 6570 syntax.
|
||||
|
||||
Supports:
|
||||
- `{var}` - simple path parameter
|
||||
- `{var*}` - wildcard path parameter (captures multiple segments)
|
||||
- `{?var1,var2}` - query parameters (ignored in path matching)
|
||||
|
||||
Hyphens in parameter names are normalized to underscores in regex group
|
||||
names so that matched groups are valid Python identifiers.
|
||||
|
||||
Returns None if the template produces an invalid regex (e.g. parameter
|
||||
names with leading digits or duplicates from a remote server).
|
||||
|
||||
|
||||
### `match_uri_template` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L83" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None
|
||||
```
|
||||
|
||||
|
||||
Match URI against template and extract both path and query parameters.
|
||||
|
||||
Supports RFC 6570 URI templates:
|
||||
- Path params: `{var}`, `{var*}`
|
||||
- Query params: `{?var1,var2}`
|
||||
|
||||
|
||||
### `expand_uri_template` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L122" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
expand_uri_template(uri_template: str, params: dict[str, Any]) -> str
|
||||
```
|
||||
|
||||
|
||||
Expand a URI template with parameters — inverse of `match_uri_template`.
|
||||
|
||||
Supports the same RFC 6570 subset:
|
||||
- Path params: `{var}`, `{var*}`
|
||||
- Query params: `{?var1,var2}`
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `ResourceTemplate` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L170" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A template for dynamically creating resources.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `resolve_security` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L203" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
resolve_security(self, server_default: ResourceSecurity | None) -> ResourceSecurity | None
|
||||
```
|
||||
|
||||
Resolve the effective security policy for this template.
|
||||
|
||||
A per-component ``security`` overrides the server default.
|
||||
``INHERIT_SECURITY`` (the field default) inherits ``server_default``;
|
||||
an explicit ``None`` disables screening for this template.
|
||||
|
||||
|
||||
#### `from_function` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L220" 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, auth: AuthCheck | list[AuthCheck] | None = None, security: ResourceSecurity | None | InheritSecurity = INHERIT_SECURITY) -> FunctionResourceTemplate
|
||||
```
|
||||
|
||||
#### `set_default_mime_type` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L253" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_default_mime_type(cls, mime_type: str | None) -> str
|
||||
```
|
||||
|
||||
Set default MIME type if not provided.
|
||||
|
||||
|
||||
#### `matches` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L259" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
matches(self, uri: str) -> dict[str, Any] | None
|
||||
```
|
||||
|
||||
Check if URI matches template and extract parameters.
|
||||
|
||||
|
||||
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L263" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult
|
||||
```
|
||||
|
||||
Read the resource content.
|
||||
|
||||
|
||||
#### `convert_result` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L269" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
convert_result(self, raw_value: Any) -> ResourceResult
|
||||
```
|
||||
|
||||
Convert a raw result to ResourceResult.
|
||||
|
||||
This is used in two contexts:
|
||||
1. In _read() to convert user function return values to ResourceResult
|
||||
2. In tasks_result_handler() to convert Docket task results to ResourceResult
|
||||
|
||||
Handles ResourceResult passthrough and converts raw values using
|
||||
ResourceResult's normalization. The template's own ``mime_type`` is
|
||||
forwarded so that reads match the MIME type the template advertises
|
||||
in ``resources/templates/list``.
|
||||
|
||||
|
||||
#### `create_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L296" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
create_resource(self, uri: str, params: dict[str, Any]) -> Resource
|
||||
```
|
||||
|
||||
Create a resource from the template with the given parameters.
|
||||
|
||||
The base implementation does not support background tasks.
|
||||
Use FunctionResourceTemplate for task support.
|
||||
|
||||
|
||||
#### `to_mcp_template` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L307" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
to_mcp_template(self, **overrides: Any) -> SDKResourceTemplate
|
||||
```
|
||||
|
||||
Convert the resource template to an SDKResourceTemplate.
|
||||
|
||||
|
||||
#### `from_mcp_template` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L327" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
from_mcp_template(cls, mcp_template: SDKResourceTemplate) -> ResourceTemplate
|
||||
```
|
||||
|
||||
Creates a FastMCP ResourceTemplate from a raw MCP ResourceTemplate object.
|
||||
|
||||
|
||||
#### `key` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L340" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
key(self) -> str
|
||||
```
|
||||
|
||||
The globally unique lookup key for this template.
|
||||
|
||||
|
||||
#### `get_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L345" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_span_attributes(self) -> dict[str, Any]
|
||||
```
|
||||
|
||||
### `FunctionResourceTemplate` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L352" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A template for dynamically creating resources.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `create_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L367" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
create_resource(self, uri: str, params: dict[str, Any]) -> Resource
|
||||
```
|
||||
|
||||
Create a resource from the template with the given parameters.
|
||||
|
||||
|
||||
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L389" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult
|
||||
```
|
||||
|
||||
Read the resource content.
|
||||
|
||||
|
||||
#### `from_function` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/template.py#L429" 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, auth: AuthCheck | list[AuthCheck] | None = None, security: ResourceSecurity | None | InheritSecurity = INHERIT_SECURITY) -> FunctionResourceTemplate
|
||||
```
|
||||
|
||||
Create a template from a function.
|
||||
|
||||
134
docs/python-sdk/fastmcp-resources-types.mdx
Normal file
134
docs/python-sdk/fastmcp-resources-types.mdx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
---
|
||||
title: types
|
||||
sidebarTitle: types
|
||||
---
|
||||
|
||||
# `fastmcp.resources.types`
|
||||
|
||||
|
||||
Concrete resource implementations.
|
||||
|
||||
## Classes
|
||||
|
||||
### `TextResource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/types.py#L21" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A resource that reads from a string.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/types.py#L26" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
read(self) -> ResourceResult
|
||||
```
|
||||
|
||||
Read the text content.
|
||||
|
||||
|
||||
### `BinaryResource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/types.py#L37" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A resource that reads from bytes.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/types.py#L42" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
read(self) -> ResourceResult
|
||||
```
|
||||
|
||||
Read the binary content.
|
||||
|
||||
|
||||
### `FileResource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/types.py#L53" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A resource that reads from a file.
|
||||
|
||||
Set is_binary=True to read file as binary data instead of text.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `validate_absolute_path` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/types.py#L83" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
validate_absolute_path(cls, path: Path) -> Path
|
||||
```
|
||||
|
||||
Ensure path is absolute.
|
||||
|
||||
|
||||
#### `set_binary_from_mime_type` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/types.py#L91" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool
|
||||
```
|
||||
|
||||
Set is_binary based on mime_type if not explicitly set.
|
||||
|
||||
|
||||
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/types.py#L99" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
read(self) -> ResourceResult
|
||||
```
|
||||
|
||||
Read the file content.
|
||||
|
||||
|
||||
### `HttpResource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/types.py#L113" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A resource that reads from an HTTP endpoint.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/types.py#L122" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
read(self) -> ResourceResult
|
||||
```
|
||||
|
||||
Read the HTTP content.
|
||||
|
||||
|
||||
### `DirectoryResource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/types.py#L134" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A resource that lists files in a directory.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `validate_absolute_path` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/types.py#L154" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
validate_absolute_path(cls, path: Path) -> Path
|
||||
```
|
||||
|
||||
Ensure path is absolute.
|
||||
|
||||
|
||||
#### `list_files` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/types.py#L160" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_files(self) -> list[Path]
|
||||
```
|
||||
|
||||
List files in the directory.
|
||||
|
||||
|
||||
#### `read` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/resources/types.py#L176" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
read(self) -> ResourceResult
|
||||
```
|
||||
|
||||
Read the directory listing.
|
||||
|
||||
48
docs/python-sdk/fastmcp-server-caching.mdx
Normal file
48
docs/python-sdk/fastmcp-server-caching.mdx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
title: caching
|
||||
sidebarTitle: caching
|
||||
---
|
||||
|
||||
# `fastmcp.server.caching`
|
||||
|
||||
|
||||
Server-level cache hints for FastMCP (SEP-2549).
|
||||
|
||||
A FastMCP server opts every SDK-cacheable result it emits into client-side
|
||||
caching by setting `cache_ttl` (seconds) and, optionally, `cache_scope` on the
|
||||
`FastMCP` constructor. The hint is uniform by construction: one server-level
|
||||
value applies to `tools/list`, `prompts/list`, `resources/list`,
|
||||
`resources/templates/list`, `resources/read`, and `server/discover` alike — no
|
||||
per-component surface and no aggregation.
|
||||
|
||||
FastMCP does not hand-set the wire fields. It passes the hint through to the SDK
|
||||
low-level `Server(cache_hints=...)`, whose runner fills `ttlMs`/`cacheScope` on
|
||||
every cacheable result via `apply_cache_hint`, leaving any field a handler set
|
||||
explicitly untouched. Honoring is modern-only and opt-in on the client: a hinted
|
||||
server is inert unless the client passes `cache=` and negotiates `2026-07-28`.
|
||||
|
||||
|
||||
## Functions
|
||||
|
||||
### `build_cache_hints` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/caching.py#L29" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
build_cache_hints(cache_ttl: int | None, cache_scope: CacheScope | None) -> dict[CacheableMethod, CacheHint] | None
|
||||
```
|
||||
|
||||
|
||||
Build the per-method `CacheHint` map for the SDK low-level server.
|
||||
|
||||
`cache_ttl` is in seconds and is converted to the wire's milliseconds. When
|
||||
`cache_ttl` is `None` the server emits no hint, so its wire output is
|
||||
identical to a server that never set one; a `cache_scope` given without a
|
||||
`cache_ttl` is meaningless (the client gates caching on the presence of a
|
||||
TTL) and is rejected rather than silently ignored.
|
||||
|
||||
Returns `None` when no hint is set, or a map applying the same hint to every
|
||||
SDK-cacheable method otherwise.
|
||||
|
||||
**Raises:**
|
||||
- `ValueError`: If `cache_ttl` is not positive, or if `cache_scope` is set
|
||||
without `cache_ttl`.
|
||||
|
||||
41
docs/python-sdk/fastmcp-server-completions.mdx
Normal file
41
docs/python-sdk/fastmcp-server-completions.mdx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
title: completions
|
||||
sidebarTitle: completions
|
||||
---
|
||||
|
||||
# `fastmcp.server.completions`
|
||||
|
||||
|
||||
Server-side argument completion for FastMCP.
|
||||
|
||||
A completion request names a reference — a specific prompt or resource
|
||||
template — and the argument being completed, plus a context of the argument
|
||||
values already supplied. The server answers with candidate string values.
|
||||
|
||||
FastMCP surfaces this as a single server-level handler registered with
|
||||
``@mcp.completion``, mirroring the MCP SDK's own ``completion/complete`` shape
|
||||
and FastMCP's client-side ``Client.complete()``. The handler receives the
|
||||
reference, the argument, and the optional context, and returns candidates for
|
||||
whichever reference/argument pair it recognizes.
|
||||
|
||||
|
||||
## Functions
|
||||
|
||||
### `normalize_completion` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/completions.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
normalize_completion(result: CompletionValues) -> mcp_types.Completion
|
||||
```
|
||||
|
||||
|
||||
Coerce a handler's return value into a wire ``Completion``.
|
||||
|
||||
A returned ``str`` is rejected: it is almost always a mistake (the value
|
||||
would iterate into one-character candidates), so it raises rather than
|
||||
silently producing surprising output.
|
||||
|
||||
The MCP contract caps a completion at 100 values, so a longer result is
|
||||
truncated to the first 100 with ``has_more`` set — a handler that returns
|
||||
thousands of matches emits a conforming response rather than an oversized
|
||||
one that strict clients reject.
|
||||
|
||||
711
docs/python-sdk/fastmcp-server-context.mdx
Normal file
711
docs/python-sdk/fastmcp-server-context.mdx
Normal file
|
|
@ -0,0 +1,711 @@
|
|||
---
|
||||
title: context
|
||||
sidebarTitle: context
|
||||
---
|
||||
|
||||
# `fastmcp.server.context`
|
||||
|
||||
## Functions
|
||||
|
||||
### `set_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L97" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_transport(transport: TransportType) -> Token[TransportType | None]
|
||||
```
|
||||
|
||||
|
||||
Set the current transport type. Returns token for reset.
|
||||
|
||||
|
||||
### `reset_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L104" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
reset_transport(token: Token[TransportType | None]) -> None
|
||||
```
|
||||
|
||||
|
||||
Reset transport to previous value.
|
||||
|
||||
|
||||
### `set_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L134" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_context(context: Context) -> Generator[Context, None, None]
|
||||
```
|
||||
|
||||
## Classes
|
||||
|
||||
### `LogData` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L110" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Data object for passing log arguments to client-side handlers.
|
||||
|
||||
This provides an interface to match the Python standard library logging,
|
||||
for compatibility with structured logging.
|
||||
|
||||
|
||||
### `Context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L143" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Context object providing access to MCP capabilities.
|
||||
|
||||
This provides a cleaner interface to MCP's RequestContext functionality.
|
||||
It gets injected into tool and resource functions that request it via type hints.
|
||||
|
||||
To use context in a tool function, add a parameter with the Context type annotation:
|
||||
|
||||
```python
|
||||
@server.tool
|
||||
async def my_tool(x: int, ctx: Context) -> str:
|
||||
# Log messages to the client
|
||||
await ctx.info(f"Processing {x}")
|
||||
await ctx.debug("Debug info")
|
||||
await ctx.warning("Warning message")
|
||||
await ctx.error("Error message")
|
||||
|
||||
# Report progress
|
||||
await ctx.report_progress(50, 100, "Processing")
|
||||
|
||||
# Access resources
|
||||
data = await ctx.read_resource("resource://data")
|
||||
|
||||
# Get request info
|
||||
request_id = ctx.request_id
|
||||
client_id = ctx.client_id
|
||||
|
||||
# Manage state across the session (persists across requests)
|
||||
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)
|
||||
```
|
||||
|
||||
State Management:
|
||||
Context provides session-scoped state that persists across requests within
|
||||
the same MCP session. State is automatically keyed by session, ensuring
|
||||
isolation between different clients.
|
||||
|
||||
State set during `on_initialize` middleware will persist to subsequent tool
|
||||
calls when using the same session object (STDIO, SSE, single-server HTTP).
|
||||
For distributed/serverless HTTP deployments where different machines handle
|
||||
the init and tool calls, state is isolated by the mcp-session-id header.
|
||||
|
||||
The context parameter name can be anything as long as it's annotated with Context.
|
||||
The context is optional - tools that don't need it can omit the parameter.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `is_background_task` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L224" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
is_background_task(self) -> bool
|
||||
```
|
||||
|
||||
True when this context is running in a background task (Docket worker).
|
||||
|
||||
When True, certain operations like elicit() will use task-aware
|
||||
implementations that can pause the task and wait for client input.
|
||||
|
||||
|
||||
#### `task_id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L242" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
task_id(self) -> str | None
|
||||
```
|
||||
|
||||
Get the background task ID if running in a background task.
|
||||
|
||||
Returns None if not running in a background task context.
|
||||
|
||||
|
||||
#### `origin_request_id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L250" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
origin_request_id(self) -> str | None
|
||||
```
|
||||
|
||||
Get the request ID that originated this execution, if available.
|
||||
|
||||
In foreground request mode, this is the current request_id.
|
||||
In background task mode, this is the request_id captured when the task
|
||||
was submitted, if one was available.
|
||||
|
||||
|
||||
#### `fastmcp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L262" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
fastmcp(self) -> FastMCP
|
||||
```
|
||||
|
||||
Get the FastMCP instance.
|
||||
|
||||
|
||||
#### `request_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L312" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
request_context(self) -> FastMCPRequestContext | None
|
||||
```
|
||||
|
||||
Access to the underlying request context.
|
||||
|
||||
Returns None when the MCP session has not been established yet.
|
||||
Returns the FastMCPRequestContext wrapper once the MCP session is available.
|
||||
|
||||
For HTTP request access in middleware, use `get_http_request()` from fastmcp.server.dependencies,
|
||||
which works whether or not the MCP session is available.
|
||||
|
||||
Example in middleware:
|
||||
```python
|
||||
async def on_request(self, context, call_next):
|
||||
ctx = context.fastmcp_context
|
||||
if ctx.request_context:
|
||||
# MCP session available - can access session_id, request_id, etc.
|
||||
session_id = ctx.session_id
|
||||
else:
|
||||
# MCP session not available yet - use HTTP helpers
|
||||
from fastmcp.server.dependencies import get_http_request
|
||||
request = get_http_request()
|
||||
return await call_next(context)
|
||||
```
|
||||
|
||||
|
||||
#### `client_extension_settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L337" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
client_extension_settings(self, identifier: str) -> dict[str, Any] | None
|
||||
```
|
||||
|
||||
This request's per-request opt-in settings for an MCP extension.
|
||||
|
||||
SEP-2133 extensions negotiate per request: the client repeats its
|
||||
extension capabilities in each request's ``_meta`` under
|
||||
``io.modelcontextprotocol/clientCapabilities`` → ``extensions`` →
|
||||
``identifier``. Returns the declared settings dict (possibly empty) when
|
||||
the extension was opted in for this request, or ``None`` when it was
|
||||
not (or there is no active request). This bridges an extension's
|
||||
``tools/call`` interceptor — which receives a FastMCP ``Context`` — to
|
||||
the request's declared client capabilities.
|
||||
|
||||
|
||||
#### `input_responses` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L378" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
input_responses(self) -> mcp_types.InputResponses | None
|
||||
```
|
||||
|
||||
Client responses to a prior `InputRequiredResult.input_requests`.
|
||||
|
||||
The multi-round-trip guard channel (SEP-2322). A guard tool inspects
|
||||
this to decide what to do on each round: `None` on the initial round
|
||||
(nothing has been asked yet, or the client retried without responses),
|
||||
so the tool returns an `InputRequiredResult` to ask; present on a later
|
||||
round, so the tool reads the answers and proceeds. It is a mapping whose
|
||||
keys match the `input_requests` map the tool minted; each value is the
|
||||
client's result for that request (an `ElicitResult`, `CreateMessageResult`,
|
||||
or `ListRootsResult`).
|
||||
|
||||
In a background task there is no wire request, so this falls back to the
|
||||
responses the in-task guard loop delivered (see the tasks extension).
|
||||
|
||||
|
||||
#### `request_state` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L399" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
request_state(self) -> str | None
|
||||
```
|
||||
|
||||
Opaque state echoed from a prior `InputRequiredResult.request_state`.
|
||||
|
||||
The multi-round-trip guard channel (SEP-2322): whatever a tool put in
|
||||
`InputRequiredResult.request_state` on an earlier round is handed back
|
||||
here (as plaintext — the framework seals it on the wire and unseals it
|
||||
before the tool runs, so tampering is rejected before this is read).
|
||||
`None` on the initial round. Use it to carry a small amount of computed
|
||||
state across rounds without re-deriving it.
|
||||
|
||||
In a background task there is no wire request, so this falls back to the
|
||||
state the in-task guard loop re-injected (see the tasks extension).
|
||||
|
||||
|
||||
#### `lifespan_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L418" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
lifespan_context(self) -> dict[str, Any]
|
||||
```
|
||||
|
||||
Access the server's lifespan context.
|
||||
|
||||
Returns the context dict yielded by *this* server's lifespan function.
|
||||
For a mounted child this is the child's own lifespan, not the parent's
|
||||
— the MCP session always belongs to the parent, so reading from the
|
||||
request context would return the parent's. We read directly from the
|
||||
server's cached lifespan result instead, which is set by the
|
||||
per-server ``_lifespan_manager`` regardless of mount position.
|
||||
|
||||
Returns an empty dict if no lifespan was configured.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@server.tool
|
||||
def my_tool(ctx: Context) -> str:
|
||||
db = ctx.lifespan_context.get("db")
|
||||
if db:
|
||||
return db.query("SELECT 1")
|
||||
return "No database connection"
|
||||
```
|
||||
|
||||
|
||||
#### `report_progress` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L453" 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
|
||||
```
|
||||
|
||||
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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L552" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_resources(self) -> list[SDKResource]
|
||||
```
|
||||
|
||||
List all available resources from the server.
|
||||
|
||||
**Returns:**
|
||||
- List of Resource objects available on the server
|
||||
|
||||
|
||||
#### `list_prompts` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L563" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_prompts(self) -> list[SDKPrompt]
|
||||
```
|
||||
|
||||
List all available prompts from the server.
|
||||
|
||||
**Returns:**
|
||||
- List of Prompt objects available on the server
|
||||
|
||||
|
||||
#### `get_prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L574" 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
|
||||
```
|
||||
|
||||
Get a prompt by name with optional arguments.
|
||||
|
||||
**Args:**
|
||||
- `name`: The name of the prompt to get
|
||||
- `arguments`: Optional arguments to pass to the prompt
|
||||
|
||||
**Returns:**
|
||||
- The prompt result
|
||||
|
||||
|
||||
#### `read_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L593" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
read_resource(self, uri: str | AnyUrl) -> ResourceResult
|
||||
```
|
||||
|
||||
Read a resource by URI.
|
||||
|
||||
**Args:**
|
||||
- `uri`: Resource URI to read
|
||||
|
||||
**Returns:**
|
||||
- ResourceResult with contents
|
||||
|
||||
|
||||
#### `log` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L609" 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
|
||||
```
|
||||
|
||||
Send a log message to the client.
|
||||
|
||||
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
|
||||
|
||||
**Args:**
|
||||
- `message`: Log message
|
||||
- `level`: Optional log level. One of "debug", "info", "notice", "warning", "error", "critical",
|
||||
"alert", or "emergency". Default is "info".
|
||||
- `logger_name`: Optional logger name
|
||||
- `extra`: Optional mapping for additional arguments
|
||||
|
||||
|
||||
#### `transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L650" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
transport(self) -> TransportType | None
|
||||
```
|
||||
|
||||
Get the current transport type.
|
||||
|
||||
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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L658" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
client_supports_extension(self, extension_id: str) -> bool
|
||||
```
|
||||
|
||||
Check whether the connected client supports a given MCP extension.
|
||||
|
||||
Inspects the ``extensions`` extra field on ``ClientCapabilities``
|
||||
sent by the client during initialization.
|
||||
|
||||
Reads the client's advertised capabilities from the session, which is
|
||||
available in request mode and in background-task mode (where the
|
||||
snapshot session preserves the client's initialize params). Returns
|
||||
``False`` when no session is available (e.g., a distributed worker with
|
||||
no live session, or outside any context) or when the client did not
|
||||
advertise the extension.
|
||||
|
||||
Example::
|
||||
|
||||
from fastmcp.apps.config import UI_EXTENSION_ID
|
||||
|
||||
@mcp.tool
|
||||
async def my_tool(ctx: Context) -> str:
|
||||
if ctx.client_supports_extension(UI_EXTENSION_ID):
|
||||
return "UI-capable client"
|
||||
return "text-only client"
|
||||
|
||||
|
||||
#### `client_id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L688" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
client_id(self) -> str | None
|
||||
```
|
||||
|
||||
Get the client ID if available.
|
||||
|
||||
|
||||
#### `request_id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L696" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
request_id(self) -> str
|
||||
```
|
||||
|
||||
Get the unique ID for this request.
|
||||
|
||||
Raises RuntimeError if MCP request context is not available.
|
||||
|
||||
|
||||
#### `session_id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L709" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
session_id(self) -> str
|
||||
```
|
||||
|
||||
Get the MCP session ID for ALL transports.
|
||||
|
||||
Returns the session ID that can be used as a key for session-based
|
||||
data storage (e.g., Redis) to share data between tool calls within
|
||||
the same client session.
|
||||
|
||||
**Returns:**
|
||||
- The session ID for StreamableHTTP transports, or a generated ID
|
||||
- for other transports.
|
||||
|
||||
|
||||
#### `session` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L794" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
session(self) -> ServerSession
|
||||
```
|
||||
|
||||
Access to the underlying session for advanced usage.
|
||||
|
||||
In request mode: Returns the session from the active request context.
|
||||
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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L820" 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
|
||||
```
|
||||
|
||||
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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L836" 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
|
||||
```
|
||||
|
||||
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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L852" 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
|
||||
```
|
||||
|
||||
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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L868" 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
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
#### `send_notification` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L884" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
send_notification(self, notification: mcp_types.ServerNotification) -> None
|
||||
```
|
||||
|
||||
Send a notification to the client immediately.
|
||||
|
||||
**Args:**
|
||||
- `notification`: An MCP notification instance (e.g., ToolListChangedNotification())
|
||||
|
||||
|
||||
#### `close_sse_stream` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L904" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
close_sse_stream(self) -> None
|
||||
```
|
||||
|
||||
Close the current response stream to trigger client reconnection.
|
||||
|
||||
When using StreamableHTTP transport with an EventStore configured, this
|
||||
method gracefully closes the HTTP connection for the current request.
|
||||
The client will automatically reconnect (after `retry_interval` milliseconds)
|
||||
and resume receiving events from where it left off via the EventStore.
|
||||
|
||||
This is useful for long-running operations to avoid load balancer timeouts.
|
||||
Instead of holding a connection open for minutes, you can periodically close
|
||||
and let the client reconnect.
|
||||
|
||||
|
||||
#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L958" 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
|
||||
```
|
||||
|
||||
The accepted elicitation will contain the response data
|
||||
|
||||
|
||||
#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L969" 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
|
||||
```
|
||||
|
||||
When response_type is a list of strings, the accepted elicitation will
|
||||
contain the selected string response
|
||||
|
||||
|
||||
#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L981" 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
|
||||
```
|
||||
|
||||
When response_type is a dict mapping keys to title dicts, the accepted
|
||||
elicitation will contain the selected key
|
||||
|
||||
|
||||
#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L993" 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
|
||||
```
|
||||
|
||||
When response_type is a list containing a list of strings (multi-select),
|
||||
the accepted elicitation will contain a list of selected strings
|
||||
|
||||
|
||||
#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1005" 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
|
||||
```
|
||||
|
||||
When response_type is a list containing a dict mapping keys to title dicts
|
||||
(multi-select with titles), the accepted elicitation will contain a list of
|
||||
selected keys
|
||||
|
||||
|
||||
#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1017" 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]]]) -> AcceptedElicitation[T] | AcceptedElicitation[dict[str, Any]] | AcceptedElicitation[str] | AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
|
||||
```
|
||||
|
||||
Send an elicitation request to the client and await the response.
|
||||
|
||||
Call this method at any time to request additional information from
|
||||
the user through the client. The client must support elicitation,
|
||||
or the request will error.
|
||||
|
||||
Note that the MCP protocol only supports simple object schemas with
|
||||
primitive types. You can provide a dataclass, TypedDict, or BaseModel to
|
||||
comply. If you provide a primitive type, an object schema with a single
|
||||
"value" field will be generated for the MCP interaction and
|
||||
automatically deconstructed into the primitive type upon response.
|
||||
|
||||
``response_type`` is required. Pass ``bool`` when all you need is a
|
||||
confirmation; an empty schema leaves some clients rendering an empty,
|
||||
non-functional form.
|
||||
|
||||
**Args:**
|
||||
- `message`: A human-readable message explaining what information is needed
|
||||
- `response_type`: The type of the response, which should be a primitive
|
||||
type or dataclass or BaseModel. If it is a primitive type, an
|
||||
object schema with a single "value" field will be generated.
|
||||
- `response_title`: Optional label to display for the wrapped ``value``
|
||||
field when ``response_type`` is a scalar, Literal, Enum, or one
|
||||
of the dict/list shorthand forms. Overrides the auto-generated
|
||||
"Value" label. Raises ``TypeError`` if passed with a BaseModel,
|
||||
dataclass, or ``None`` response type (use ``Field(title=...)``
|
||||
on the model instead).
|
||||
- `response_description`: Optional description to attach to the wrapped
|
||||
``value`` field. Same scope rules as ``response_title``.
|
||||
|
||||
|
||||
#### `set_state` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1110" 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 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.
|
||||
|
||||
The key is automatically prefixed with the session identifier.
|
||||
|
||||
|
||||
#### `get_state` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1164" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_state(self, key: str) -> Any
|
||||
```
|
||||
|
||||
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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1178" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
delete_state(self, key: str) -> None
|
||||
```
|
||||
|
||||
Delete a value from the state store.
|
||||
|
||||
Removes from both request-scoped and session-scoped stores.
|
||||
|
||||
|
||||
#### `enable_components` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1199" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
enable_components(self) -> None
|
||||
```
|
||||
|
||||
Enable components matching criteria for this session only.
|
||||
|
||||
Session rules override global transforms. Rules accumulate - each call
|
||||
adds a new rule to the session. Later marks override earlier ones
|
||||
(Visibility transform semantics).
|
||||
|
||||
Sends notifications to this session only: ToolListChangedNotification,
|
||||
ResourceListChangedNotification, and PromptListChangedNotification.
|
||||
|
||||
**Args:**
|
||||
- `names`: Component names or URIs to match.
|
||||
- `keys`: Component keys to match (e.g., {"tool\:my_tool@v1"}).
|
||||
- `version`: Component version spec to match.
|
||||
- `tags`: Tags to match (component must have at least one).
|
||||
- `components`: Component types to match (e.g., {"tool", "prompt"}).
|
||||
- `match_all`: If True, matches all components regardless of other criteria.
|
||||
|
||||
|
||||
#### `disable_components` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1237" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
disable_components(self) -> None
|
||||
```
|
||||
|
||||
Disable components matching criteria for this session only.
|
||||
|
||||
Session rules override global transforms. Rules accumulate - each call
|
||||
adds a new rule to the session. Later marks override earlier ones
|
||||
(Visibility transform semantics).
|
||||
|
||||
Sends notifications to this session only: ToolListChangedNotification,
|
||||
ResourceListChangedNotification, and PromptListChangedNotification.
|
||||
|
||||
**Args:**
|
||||
- `names`: Component names or URIs to match.
|
||||
- `keys`: Component keys to match (e.g., {"tool\:my_tool@v1"}).
|
||||
- `version`: Component version spec to match.
|
||||
- `tags`: Tags to match (component must have at least one).
|
||||
- `components`: Component types to match (e.g., {"tool", "prompt"}).
|
||||
- `match_all`: If True, matches all components regardless of other criteria.
|
||||
|
||||
|
||||
#### `reset_visibility` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1275" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
reset_visibility(self) -> None
|
||||
```
|
||||
|
||||
Clear all session visibility rules.
|
||||
|
||||
Use this to reset session visibility back to global defaults.
|
||||
|
||||
Sends notifications to this session only: ToolListChangedNotification,
|
||||
ResourceListChangedNotification, and PromptListChangedNotification.
|
||||
|
||||
617
docs/python-sdk/fastmcp-server-dependencies.mdx
Normal file
617
docs/python-sdk/fastmcp-server-dependencies.mdx
Normal file
|
|
@ -0,0 +1,617 @@
|
|||
---
|
||||
title: dependencies
|
||||
sidebarTitle: dependencies
|
||||
---
|
||||
|
||||
# `fastmcp.server.dependencies`
|
||||
|
||||
|
||||
Dependency injection for FastMCP.
|
||||
|
||||
DI features (Depends, CurrentContext, CurrentFastMCP) work without pydocket
|
||||
using the uncalled-for DI engine. The docket-specific dependencies
|
||||
(``CurrentDocket``, ``CurrentWorker``) and background task execution live in the
|
||||
``fastmcp-tasks`` package.
|
||||
|
||||
|
||||
## Functions
|
||||
|
||||
### `bind_request_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L109" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
bind_request_context(ctx: ServerRequestContext) -> Generator[FastMCPRequestContext, None, None]
|
||||
```
|
||||
|
||||
|
||||
Bind a ``FastMCPRequestContext`` for the duration of a handler.
|
||||
|
||||
Constructs the wrapper from the SDK's per-request context and sets/resets
|
||||
the ``fastmcp_request_ctx`` ContextVar. Every request adapter and the
|
||||
initialize middleware enters this so ``Context`` and dependency helpers can
|
||||
read the active request from the ContextVar.
|
||||
|
||||
|
||||
### `extract_version_spec` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L136" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
extract_version_spec(meta: dict[str, Any] | None) -> str | None
|
||||
```
|
||||
|
||||
|
||||
Extract the FastMCP component version from a lifted ``_meta`` block.
|
||||
|
||||
|
||||
### `set_background_context_factory` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L190" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_background_context_factory(factory: Callable[[], Awaitable[Context | None]] | None) -> None
|
||||
```
|
||||
|
||||
|
||||
Install (or clear) the background-task ``Context`` factory.
|
||||
|
||||
The factory returns an already-entered ``Context`` (so ``_current_context``
|
||||
is set for cleanup) when called inside a worker, or ``None`` when there is
|
||||
no task context. Passing ``None`` restores core's no-worker-fallback
|
||||
behavior.
|
||||
|
||||
|
||||
### `set_worker_server_resolver` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L212" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_worker_server_resolver(resolver: Callable[[], FastMCP | None] | None) -> None
|
||||
```
|
||||
|
||||
|
||||
Install (or clear) the worker-server resolver used by ``get_server()``.
|
||||
|
||||
|
||||
### `is_docket_available` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L249" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
is_docket_available() -> bool
|
||||
```
|
||||
|
||||
|
||||
Check if a compatible pydocket (>= 0.19.0) is installed and importable.
|
||||
|
||||
Three things have to be true for fastmcp's task features to work:
|
||||
1. pydocket distribution metadata is discoverable
|
||||
2. its version is at least ``_MIN_DOCKET_VERSION`` (older versions are
|
||||
missing symbols like ``docket.dependencies.current_execution``,
|
||||
which fastmcp imports on the request hot path)
|
||||
3. the package actually imports — guards against broken/partial
|
||||
installs where metadata exists but ``import docket`` blows up
|
||||
|
||||
Any of those failing means we treat docket as unavailable and fall back
|
||||
to the no-tasks code paths instead of crashing deep inside a request.
|
||||
|
||||
|
||||
### `transform_context_annotations` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L281" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
transform_context_annotations(fn: Callable[..., Any]) -> Callable[..., Any]
|
||||
```
|
||||
|
||||
|
||||
Transform injected-by-type params into Dependency-defaulted params.
|
||||
|
||||
Transforms ALL params typed as Context (into ``= CurrentContext()``) and as
|
||||
UserSession (into ``= CurrentSession()``) to use Docket's DI system, unless
|
||||
they already have a Dependency-based default.
|
||||
|
||||
This unifies the legacy type annotation DI with Docket's Depends() system,
|
||||
allowing both patterns to work through a single resolution path.
|
||||
|
||||
Note: Only POSITIONAL_OR_KEYWORD parameters are reordered (params with defaults
|
||||
after those without). KEYWORD_ONLY parameters keep their position since Python
|
||||
allows them to have defaults in any order.
|
||||
|
||||
**Args:**
|
||||
- `fn`: Function to transform
|
||||
|
||||
**Returns:**
|
||||
- Function with modified signature (same function object, updated __signature__)
|
||||
|
||||
|
||||
### `get_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L447" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_context() -> Context
|
||||
```
|
||||
|
||||
|
||||
Get the current FastMCP Context instance directly.
|
||||
|
||||
|
||||
### `get_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L457" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_server() -> FastMCP
|
||||
```
|
||||
|
||||
|
||||
Get the current FastMCP server instance directly.
|
||||
|
||||
In a background-task worker the tasks extension's resolver is consulted
|
||||
first, so a mounted-child task resolves to the child server rather than the
|
||||
root that started the worker (#3571).
|
||||
|
||||
**Returns:**
|
||||
- The active FastMCP server
|
||||
|
||||
**Raises:**
|
||||
- `RuntimeError`: If no server in context
|
||||
|
||||
|
||||
### `get_session` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L485" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_session(session_id: str) -> Session
|
||||
```
|
||||
|
||||
|
||||
Resolve and validate a `Session` for an explicit `session_id`.
|
||||
|
||||
Pair with a `session_id: SessionId` tool argument (the agent obtains an id
|
||||
from `create_session` and passes it back). For a single per-user bucket with
|
||||
nothing for the agent to pass, inject `session: UserSession` instead.
|
||||
|
||||
State is keyed by `(principal, session_id)`: the authenticated principal is
|
||||
the isolation wall and `session_id` organizes sessions within it. The id must
|
||||
have been minted by `create_session` under the current principal; an id that
|
||||
was never created, or created under a different principal, raises
|
||||
`InvalidSession` rather than resolving to a fresh empty bucket (the specific
|
||||
reason is logged at debug level, never returned to the caller).
|
||||
|
||||
Like `get_server()`, this resolves through the task-aware server, so it needs
|
||||
no foreground context — it works from a `task=True` tool's Docket worker as
|
||||
well as a normal request.
|
||||
|
||||
|
||||
### `get_http_request` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L520" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_http_request() -> Request
|
||||
```
|
||||
|
||||
|
||||
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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L541" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_http_headers(include_all: bool = False, include: set[str] | None = None) -> dict[str, str]
|
||||
```
|
||||
|
||||
|
||||
Extract headers from the current HTTP request if available.
|
||||
|
||||
Never raises an exception, even if there is no active HTTP request (in which case
|
||||
an empty dict is returned).
|
||||
|
||||
By default, strips problematic headers like `content-length` and `authorization`
|
||||
that cause issues if forwarded to downstream services. If `include_all` is True,
|
||||
all headers are returned.
|
||||
|
||||
The `include` parameter allows specific headers to be included even if they would
|
||||
normally be excluded. This is useful for proxy transports that need to forward
|
||||
authorization headers to upstream MCP servers.
|
||||
|
||||
|
||||
### `get_access_token` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L605" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_access_token() -> AccessToken | None
|
||||
```
|
||||
|
||||
|
||||
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.
|
||||
|
||||
**Returns:**
|
||||
- The access token if an authenticated user is available, None otherwise.
|
||||
|
||||
|
||||
### `without_injected_parameters` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L664" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
without_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any]
|
||||
```
|
||||
|
||||
|
||||
Create a wrapper function without injected parameters.
|
||||
|
||||
Returns a wrapper that excludes Context and Docket dependency parameters,
|
||||
making it safe to use with Pydantic TypeAdapter for schema generation and
|
||||
validation. The wrapper internally handles all dependency resolution and
|
||||
Context injection when called.
|
||||
|
||||
Handles:
|
||||
- Legacy Context injection (always works)
|
||||
- Depends() injection (always works - uses docket or vendored DI engine)
|
||||
|
||||
**Args:**
|
||||
- `fn`: Original function with Context and/or dependencies
|
||||
- `run_in_thread`: For sync ``fn``, whether to dispatch the call to a worker
|
||||
thread after resolving dependencies. Defaults to True. Set to False
|
||||
to call ``fn`` inline on the event loop thread — required for
|
||||
thread-affinity libraries (e.g. Windows COM). Ignored for async fns.
|
||||
|
||||
**Returns:**
|
||||
- Async wrapper function without injected parameters
|
||||
|
||||
|
||||
### `resolve_dependencies` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L828" 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]
|
||||
```
|
||||
|
||||
|
||||
Resolve dependencies for a FastMCP function.
|
||||
|
||||
This function:
|
||||
1. Filters out any dependency parameter names from user arguments (security)
|
||||
2. Resolves Depends() parameters via the DI system
|
||||
|
||||
The filtering prevents external callers from overriding injected parameters by
|
||||
providing values for dependency parameter names. This is a security feature.
|
||||
The filtered arguments also feed the resolution frame, so a CallArgument()
|
||||
reference to a dependency parameter resolves the dependency and never a
|
||||
caller-supplied value.
|
||||
|
||||
Note: Context injection is handled via transform_context_annotations() which
|
||||
converts `ctx: Context` to `ctx: Context = Depends(get_context)` at registration
|
||||
time, so all injection goes through the unified DI system.
|
||||
|
||||
**Args:**
|
||||
- `fn`: The function to resolve dependencies for
|
||||
- `arguments`: User arguments (may contain keys that match dependency names,
|
||||
which will be filtered out)
|
||||
|
||||
|
||||
### `CurrentContext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L956" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
CurrentContext() -> Context
|
||||
```
|
||||
|
||||
|
||||
Get the current FastMCP Context instance.
|
||||
|
||||
This dependency provides access to the active FastMCP Context for the
|
||||
current MCP operation (tool/resource/prompt call).
|
||||
|
||||
**Returns:**
|
||||
- A dependency that resolves to the active Context instance
|
||||
|
||||
**Raises:**
|
||||
- `RuntimeError`: If no active context found (during resolution)
|
||||
|
||||
|
||||
### `OptionalCurrentContext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L981" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
OptionalCurrentContext() -> Context | None
|
||||
```
|
||||
|
||||
|
||||
Get the current FastMCP Context, or None when no context is active.
|
||||
|
||||
|
||||
### `CurrentFastMCP` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1001" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
CurrentFastMCP() -> FastMCP
|
||||
```
|
||||
|
||||
|
||||
Get the current FastMCP server instance.
|
||||
|
||||
This dependency provides access to the active FastMCP server.
|
||||
|
||||
**Returns:**
|
||||
- A dependency that resolves to the active FastMCP server
|
||||
|
||||
**Raises:**
|
||||
- `RuntimeError`: If no server in context (during resolution)
|
||||
|
||||
|
||||
### `CurrentRequest` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1041" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
CurrentRequest() -> Request
|
||||
```
|
||||
|
||||
|
||||
Get the current HTTP request.
|
||||
|
||||
This dependency provides access to the Starlette Request object for the
|
||||
current HTTP request. Only available when running over HTTP transports
|
||||
(SSE or Streamable HTTP).
|
||||
|
||||
**Returns:**
|
||||
- A dependency that resolves to the active Starlette Request
|
||||
|
||||
**Raises:**
|
||||
- `RuntimeError`: If no HTTP request in context (e.g., STDIO transport)
|
||||
|
||||
|
||||
### `CurrentHeaders` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1082" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
CurrentHeaders() -> dict[str, str]
|
||||
```
|
||||
|
||||
|
||||
Get the current HTTP request headers.
|
||||
|
||||
This dependency provides access to the HTTP headers for the current request,
|
||||
including the authorization header. Returns an empty dictionary when no HTTP
|
||||
request is available, making it safe to use in code that might run over any
|
||||
transport.
|
||||
|
||||
**Returns:**
|
||||
- A dependency that resolves to a dictionary of header name -> value
|
||||
|
||||
|
||||
### `CurrentAccessToken` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1300" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
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)
|
||||
|
||||
|
||||
### `TokenClaim` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1357" 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
|
||||
|
||||
### `FastMCPRequestContext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L60" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
FastMCP-owned wrapper around the SDK's per-request context.
|
||||
|
||||
The SDK v2 runner hands each handler a fresh ``ServerRequestContext`` as an
|
||||
argument rather than exposing it through a ContextVar. FastMCP owns this
|
||||
ContextVar (``fastmcp_request_ctx``) and each request adapter binds a
|
||||
``FastMCPRequestContext`` at the top of the handler (and the initialize
|
||||
middleware binds it too).
|
||||
|
||||
A wrapper rather than the raw context because the SDK's
|
||||
``ServerRequestContext.meta`` is a bare ``RequestParamsMeta`` TypedDict that
|
||||
only carries ``progress_token`` — it does not carry ``_meta.fastmcp`` or the
|
||||
distributed-trace parent. Those live in the raw params dict under ``_meta``,
|
||||
which this wrapper lifts once so downstream consumers have a stable surface.
|
||||
|
||||
|
||||
### `ProgressLike` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1110" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Protocol for progress tracking interface.
|
||||
|
||||
Defines the common interface between InMemoryProgress (server context)
|
||||
and Docket's Progress (worker context).
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `current` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1118" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
current(self) -> int | None
|
||||
```
|
||||
|
||||
Current progress value.
|
||||
|
||||
|
||||
#### `total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1123" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
total(self) -> int
|
||||
```
|
||||
|
||||
Total/target progress value.
|
||||
|
||||
|
||||
#### `message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1128" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
message(self) -> str | None
|
||||
```
|
||||
|
||||
Current progress message.
|
||||
|
||||
|
||||
#### `set_total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1132" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_total(self, total: int) -> None
|
||||
```
|
||||
|
||||
Set the total/target value for progress tracking.
|
||||
|
||||
|
||||
#### `increment` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1136" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
increment(self, amount: int = 1) -> None
|
||||
```
|
||||
|
||||
Atomically increment the current progress value.
|
||||
|
||||
|
||||
#### `set_message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1140" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_message(self, message: str | None) -> None
|
||||
```
|
||||
|
||||
Update the progress status message.
|
||||
|
||||
|
||||
### `InMemoryProgress` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1145" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
In-memory progress tracker for immediate tool execution.
|
||||
|
||||
Provides the same interface as Docket's Progress but stores state in memory
|
||||
instead of Redis. Useful for testing and immediate execution where
|
||||
progress doesn't need to be observable across processes.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `current` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1170" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
current(self) -> int | None
|
||||
```
|
||||
|
||||
#### `total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1174" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
total(self) -> int
|
||||
```
|
||||
|
||||
#### `message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1178" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1181" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_total(self, total: int) -> None
|
||||
```
|
||||
|
||||
Set the total/target value for progress tracking.
|
||||
|
||||
|
||||
#### `increment` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1187" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
increment(self, amount: int = 1) -> None
|
||||
```
|
||||
|
||||
Atomically increment the current progress value.
|
||||
|
||||
|
||||
#### `set_message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1196" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_message(self, message: str | None) -> None
|
||||
```
|
||||
|
||||
Update the progress status message.
|
||||
|
||||
|
||||
### `Progress` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1201" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Progress dependency that works in both server and worker contexts.
|
||||
|
||||
In a Docket worker, delegates to the execution's Redis-backed progress
|
||||
(observable across processes). Otherwise, uses in-memory tracking.
|
||||
|
||||
The shared default instance acts as a stateless factory — ``__aenter__``
|
||||
creates a fresh ``Progress`` per invocation so concurrent tasks never
|
||||
share mutable state.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `current` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1242" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
current(self) -> int | None
|
||||
```
|
||||
|
||||
Current progress value.
|
||||
|
||||
|
||||
#### `total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1248" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
total(self) -> int
|
||||
```
|
||||
|
||||
Total/target progress value.
|
||||
|
||||
|
||||
#### `message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1254" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
message(self) -> str | None
|
||||
```
|
||||
|
||||
Current progress message.
|
||||
|
||||
|
||||
#### `set_total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1259" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_total(self, total: int) -> None
|
||||
```
|
||||
|
||||
Set the total/target value for progress tracking.
|
||||
|
||||
|
||||
#### `increment` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1264" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
increment(self, amount: int = 1) -> None
|
||||
```
|
||||
|
||||
Atomically increment the current progress value.
|
||||
|
||||
|
||||
#### `set_message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1269" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_message(self, message: str | None) -> None
|
||||
```
|
||||
|
||||
Update the progress status message.
|
||||
|
||||
152
docs/python-sdk/fastmcp-server-elicitation.mdx
Normal file
152
docs/python-sdk/fastmcp-server-elicitation.mdx
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
---
|
||||
title: elicitation
|
||||
sidebarTitle: elicitation
|
||||
---
|
||||
|
||||
# `fastmcp.server.elicitation`
|
||||
|
||||
## Functions
|
||||
|
||||
### `parse_elicit_response_type` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L143" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
parse_elicit_response_type(response_type: Any, response_title: str | None = None, response_description: str | None = None) -> ElicitConfig
|
||||
```
|
||||
|
||||
|
||||
Parse response_type into schema and handling configuration.
|
||||
|
||||
A response type is required; ``None`` raises ``TypeError``. Supports
|
||||
multiple syntaxes:
|
||||
- dict: `{"low": {"title": "..."}}` -> single-select titled enum
|
||||
- list patterns:
|
||||
- `[["a", "b"]]` -> multi-select untitled
|
||||
- `[{"low": {...}}]` -> multi-select titled
|
||||
- `["a", "b"]` -> single-select untitled
|
||||
- `list\[X]` type annotation: multi-select with type
|
||||
- Scalar types (bool, int, float, str, Literal, Enum): single value
|
||||
- Other types (dataclass, BaseModel): use directly
|
||||
|
||||
The ``response_title`` and ``response_description`` arguments customize the
|
||||
label and description of the wrapped ``value`` property for the scalar/dict/list
|
||||
shorthand forms. They are only valid when FastMCP is wrapping the response
|
||||
type; passing them with a full BaseModel/dataclass raises ``TypeError``,
|
||||
because in those cases the user already controls field metadata via
|
||||
``Field(title=..., description=...)``.
|
||||
|
||||
|
||||
### `handle_elicit_accept` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L310" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
handle_elicit_accept(config: ElicitConfig, content: Any) -> AcceptedElicitation[Any]
|
||||
```
|
||||
|
||||
|
||||
Handle an accepted elicitation response.
|
||||
|
||||
**Args:**
|
||||
- `config`: The elicitation configuration from parse_elicit_response_type
|
||||
- `content`: The response content from the client
|
||||
|
||||
**Returns:**
|
||||
- AcceptedElicitation with the extracted/validated data
|
||||
|
||||
|
||||
### `get_elicitation_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L369" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_elicitation_schema(response_type: type[T]) -> dict[str, Any]
|
||||
```
|
||||
|
||||
|
||||
Get the schema for an elicitation response.
|
||||
|
||||
**Args:**
|
||||
- `response_type`: The type of the response
|
||||
|
||||
|
||||
### `validate_elicitation_json_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L395" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
validate_elicitation_json_schema(schema: dict[str, Any]) -> None
|
||||
```
|
||||
|
||||
|
||||
Validate that a JSON schema follows MCP elicitation requirements.
|
||||
|
||||
This ensures the schema is compatible with MCP elicitation requirements:
|
||||
- Must be an object schema
|
||||
- Must only contain primitive field types (string, number, integer, boolean)
|
||||
- Must be flat (no nested objects or arrays of objects)
|
||||
- Allows const fields (for Literal types) and enum fields (for Enum types)
|
||||
- Only primitive types and their nullable variants are allowed
|
||||
|
||||
**Args:**
|
||||
- `schema`: The JSON schema to validate
|
||||
|
||||
**Raises:**
|
||||
- `TypeError`: If the schema doesn't meet MCP elicitation requirements
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `ElicitationJsonSchema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L36" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Custom JSON schema generator for MCP elicitation that always inlines enums.
|
||||
|
||||
MCP elicitation requires inline enum schemas without $ref/$defs references.
|
||||
This generator ensures enums are always generated inline for compatibility.
|
||||
Optionally adds enumNames for better UI display when available.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `generate_inner` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L44" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
generate_inner(self, schema: core_schema.CoreSchema) -> JsonSchemaValue
|
||||
```
|
||||
|
||||
Override to prevent ref generation for enums and handle list schemas.
|
||||
|
||||
|
||||
#### `list_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L57" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_schema(self, schema: core_schema.ListSchema) -> JsonSchemaValue
|
||||
```
|
||||
|
||||
Generate schema for list types, detecting enum items for multi-select.
|
||||
|
||||
|
||||
#### `enum_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L94" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
enum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue
|
||||
```
|
||||
|
||||
Generate inline enum schema.
|
||||
|
||||
Always generates enum pattern: `{"enum": [value, ...]}`
|
||||
Titled enums are handled separately via dict-based syntax in ctx.elicit().
|
||||
|
||||
|
||||
### `AcceptedElicitation` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L105" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Result when user accepts the elicitation.
|
||||
|
||||
|
||||
### `ScalarElicitationType` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L113" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
### `ElicitConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L118" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Configuration for an elicitation request.
|
||||
|
||||
**Attributes:**
|
||||
- `schema`: The JSON schema to send to the client
|
||||
- `response_type`: The type to validate responses with (None for raw schemas)
|
||||
- `is_raw`: True if schema was built directly (extract "value" from response)
|
||||
|
||||
78
docs/python-sdk/fastmcp-server-event_store.mdx
Normal file
78
docs/python-sdk/fastmcp-server-event_store.mdx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
title: event_store
|
||||
sidebarTitle: event_store
|
||||
---
|
||||
|
||||
# `fastmcp.server.event_store`
|
||||
|
||||
|
||||
EventStore implementation backed by AsyncKeyValue.
|
||||
|
||||
This module provides an EventStore implementation that enables SSE polling/resumability
|
||||
for Streamable HTTP transports. Events are stored using the key_value package's
|
||||
AsyncKeyValue protocol, allowing users to configure any compatible backend
|
||||
(in-memory, Redis, etc.) following the same pattern as ResponseCachingMiddleware.
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `EventEntry` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L38" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Stored event entry.
|
||||
|
||||
|
||||
### `StreamEventList` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L46" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
List of event IDs for a stream.
|
||||
|
||||
|
||||
### `EventStore` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L52" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
EventStore implementation backed by AsyncKeyValue.
|
||||
|
||||
Enables SSE polling/resumability by storing events that can be replayed
|
||||
when clients reconnect. Works with any AsyncKeyValue backend (memory, Redis, etc.)
|
||||
following the same pattern as ResponseCachingMiddleware and OAuthProxy.
|
||||
|
||||
**Args:**
|
||||
- `storage`: AsyncKeyValue backend. Defaults to MemoryStore.
|
||||
- `max_events_per_stream`: Maximum events to retain per stream. Default 100.
|
||||
- `ttl`: Event TTL in seconds. Default 3600 (1 hour). Set to None for no expiration.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `store_event` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L120" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId
|
||||
```
|
||||
|
||||
Store an event and return its ID.
|
||||
|
||||
**Args:**
|
||||
- `stream_id`: ID of the stream the event belongs to
|
||||
- `message`: The JSON-RPC message to store, or None for priming events
|
||||
|
||||
**Returns:**
|
||||
- The generated event ID for the stored event
|
||||
|
||||
|
||||
#### `replay_events_after` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L166" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
replay_events_after(self, last_event_id: EventId, send_callback: EventCallback) -> StreamId | None
|
||||
```
|
||||
|
||||
Replay events that occurred after the specified event ID.
|
||||
|
||||
**Args:**
|
||||
- `last_event_id`: The ID of the last event the client received
|
||||
- `send_callback`: A callback function to send events to the client
|
||||
|
||||
**Returns:**
|
||||
- The stream ID of the replayed events, or None if the event ID was not found
|
||||
|
||||
194
docs/python-sdk/fastmcp-server-extensions.mdx
Normal file
194
docs/python-sdk/fastmcp-server-extensions.mdx
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
---
|
||||
title: extensions
|
||||
sidebarTitle: extensions
|
||||
---
|
||||
|
||||
# `fastmcp.server.extensions`
|
||||
|
||||
|
||||
FastMCP-native server extension API (SEP-2133).
|
||||
|
||||
An MCP extension is an opt-in, capability-negotiated bundle of protocol
|
||||
behaviour identified by a reverse-DNS string (e.g. `io.modelcontextprotocol/tasks`).
|
||||
Unlike the SDK's `mcp.server.extension.Extension`, a FastMCP `ServerExtension`
|
||||
is bound to its `FastMCP` instance at registration, so its request handlers and
|
||||
its `tools/call` interceptor can reach the component registry, `Context`, and
|
||||
auth scope that the SDK's model withholds.
|
||||
|
||||
An extension contributes any subset of four things:
|
||||
|
||||
- **A negotiated capability.** `settings()` is spliced into
|
||||
`ServerCapabilities.extensions[identifier]` (see `LowLevelServer.get_capabilities`).
|
||||
- **New request methods.** `methods()` returns `MethodBinding`s, each wired onto
|
||||
the low-level server via `add_request_handler` when the extension is registered.
|
||||
- **A `tools/call` interceptor.** `intercept_tool_call()` is the last gate before
|
||||
a tool body runs — it composes *after* the FastMCP middleware chain and *before*
|
||||
component execution, so it can observe, short-circuit, or pass a call through.
|
||||
- **A lifespan.** `lifespan()` is entered with the server's lifespan and exited on
|
||||
shutdown — the hook the SDK's `Extension` lacks, needed to start backends/workers.
|
||||
|
||||
The base class follows the SDK's httpx-style shape: every contribution method has
|
||||
a default, so a subclass overrides only what it needs.
|
||||
|
||||
|
||||
## Functions
|
||||
|
||||
### `read_client_extension_settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L235" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
read_client_extension_settings(ctx: ServerRequestContext[Any, Any], identifier: str) -> dict[str, Any] | None
|
||||
```
|
||||
|
||||
|
||||
Read a client's per-request extension opt-in from the request `_meta`.
|
||||
|
||||
SEP-2133 extensions negotiate per request: the client repeats its extension
|
||||
capabilities in each request's `_meta` under
|
||||
`io.modelcontextprotocol/clientCapabilities` → `extensions` → `identifier`.
|
||||
Returns the declared settings dict (possibly empty) when the extension was
|
||||
opted in for this request, or `None` when it was not.
|
||||
|
||||
|
||||
### `build_method_handler` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L249" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
build_method_handler(binding: MethodBinding) -> ExtensionRequestHandler
|
||||
```
|
||||
|
||||
|
||||
Wrap a `MethodBinding` into a low-level request handler.
|
||||
|
||||
The adapter enforces `protocol_versions` gating (rejecting other versions as
|
||||
`METHOD_NOT_FOUND`, since `add_request_handler` registers unconditionally)
|
||||
and binds the FastMCP request context so the handler can use `get_context()`,
|
||||
auth, and other request-scoped dependencies.
|
||||
|
||||
|
||||
### `wrap_tool_call_interceptor` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L278" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
wrap_tool_call_interceptor(extension: ServerExtension, call_next: Callable[[Any], Awaitable[Any]]) -> Callable[[Any], Awaitable[Any]]
|
||||
```
|
||||
|
||||
|
||||
Fold one extension's `intercept_tool_call` around a middleware `call_next`.
|
||||
|
||||
The returned wrapper is a FastMCP `CallNext`: it hands the extension the
|
||||
validated `tools/call` params, the FastMCP `Context`, and a zero-arg
|
||||
continuation that runs the rest of the chain and, finally, the tool body.
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `MethodBinding` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L77" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A new request method an extension serves, e.g. `tasks/get`.
|
||||
|
||||
`params_type` validates incoming params before `handler` runs; it should
|
||||
subclass `RequestParams` so `_meta` parses uniformly. `protocol_versions`,
|
||||
when set, restricts the method to those wire versions — a request at any
|
||||
other version is rejected as `METHOD_NOT_FOUND`, mirroring the spec's
|
||||
`(method, version)` boundary. `None` (the default) admits every version.
|
||||
|
||||
Extension methods are additive: `method` must not name a spec-defined
|
||||
request method (`tools/call`, `completion/complete`, ...). Binding one would
|
||||
silently shadow the server's own handler. Both constraints are enforced at
|
||||
construction.
|
||||
|
||||
|
||||
### `ServerExtension` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L111" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Base class for an opt-in FastMCP server extension (SEP-2133).
|
||||
|
||||
Subclass, set `identifier`, and override the contribution methods that
|
||||
apply. Every method has a default, so a minimal extension overrides only
|
||||
`identifier` and one contribution. `identifier` is validated at
|
||||
subclass-definition time when set as a class attribute, and again at
|
||||
registration (which covers per-instance identifiers assigned in `__init__`).
|
||||
|
||||
Register an instance with `FastMCP.add_extension(...)`, which binds the
|
||||
extension to the server so `self.server`, `intercept_tool_call`, and method
|
||||
handlers can reach FastMCP-level constructs.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L149" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
server(self) -> FastMCP
|
||||
```
|
||||
|
||||
The FastMCP server this extension is registered on.
|
||||
|
||||
Handlers, interceptors, and lifespan code reach the component registry,
|
||||
`Context`, and auth scope through here. Raises if the extension has not
|
||||
been registered with `FastMCP.add_extension()`.
|
||||
|
||||
|
||||
#### `settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L165" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
settings(self) -> dict[str, Any]
|
||||
```
|
||||
|
||||
Per-extension settings advertised at `capabilities.extensions[identifier]`.
|
||||
|
||||
An empty dict (the default) advertises the extension with no settings.
|
||||
|
||||
|
||||
#### `methods` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L172" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
methods(self) -> Sequence[MethodBinding]
|
||||
```
|
||||
|
||||
New request methods this extension serves (additive).
|
||||
|
||||
|
||||
#### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L176" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
lifespan(self) -> AbstractAsyncContextManager[None]
|
||||
```
|
||||
|
||||
A context manager entered with the server's lifespan, exited on shutdown.
|
||||
|
||||
Default: a no-op. Override to start and stop resources an extension owns
|
||||
(a task-queue backend and worker, say). Entered once per runtime tree, at
|
||||
the root — a mounted child defers to the root, as the shared Docket does.
|
||||
|
||||
|
||||
#### `intercept_tool_call` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L185" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
intercept_tool_call(self, params: CallToolRequestParams, context: Context, call_next: ToolCallContinuation) -> ToolCallOutcome
|
||||
```
|
||||
|
||||
Wrap `tools/call`. Default: pass through unchanged.
|
||||
|
||||
Runs after the FastMCP middleware chain and before the tool body, so it
|
||||
is the last gate before execution. Override to observe the call, to
|
||||
short-circuit (return a result without awaiting `call_next`), or to pass
|
||||
it through (`return await call_next()`). `params` is the validated
|
||||
`tools/call` params; `context` is the FastMCP `Context`, from which the
|
||||
tool being called (`context.fastmcp.get_tool(params.name)`), auth scope,
|
||||
and the server are reachable. Multiple extensions nest with the
|
||||
first-registered outermost.
|
||||
|
||||
|
||||
#### `client_settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L204" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
client_settings(self, ctx: ServerRequestContext[Any, Any]) -> dict[str, Any] | None
|
||||
```
|
||||
|
||||
This extension's per-request opt-in settings declared by the client.
|
||||
|
||||
Reads the request's `_meta` client-capabilities block. Returns the
|
||||
declared settings dict (possibly empty) when the client opted this
|
||||
extension in for the request, or `None` when it did not. Convenience for
|
||||
`read_client_extension_settings(ctx, self.identifier)`.
|
||||
|
||||
144
docs/python-sdk/fastmcp-server-http.mdx
Normal file
144
docs/python-sdk/fastmcp-server-http.mdx
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
---
|
||||
title: http
|
||||
sidebarTitle: http
|
||||
---
|
||||
|
||||
# `fastmcp.server.http`
|
||||
|
||||
## Functions
|
||||
|
||||
### `set_http_request` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L355" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_http_request(request: Request) -> Generator[Request, None, None]
|
||||
```
|
||||
|
||||
### `create_base_app` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L388" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
create_base_app(routes: list[BaseRoute], middleware: list[Middleware], debug: bool = False, lifespan: Callable | None = None) -> StarletteWithLifespan
|
||||
```
|
||||
|
||||
|
||||
Create a base Starlette app with common middleware and routes.
|
||||
|
||||
**Args:**
|
||||
- `routes`: List of routes to include in the app
|
||||
- `middleware`: List of middleware to include in the app
|
||||
- `debug`: Whether to enable debug mode
|
||||
- `lifespan`: Optional lifespan manager for the app
|
||||
|
||||
**Returns:**
|
||||
- A Starlette application
|
||||
|
||||
|
||||
### `create_sse_app` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L416" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
create_sse_app(server: FastMCP[LifespanResultT], message_path: str, sse_path: str, auth: AuthProvider | None = None, debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None) -> StarletteWithLifespan
|
||||
```
|
||||
|
||||
|
||||
Return an instance of the SSE server app.
|
||||
|
||||
**Args:**
|
||||
- `server`: The FastMCP server instance
|
||||
- `message_path`: Path for SSE messages
|
||||
- `sse_path`: Path for SSE connections
|
||||
- `auth`: Optional authentication provider (AuthProvider)
|
||||
- `debug`: Whether to enable debug mode
|
||||
- `routes`: Optional list of custom routes
|
||||
- `middleware`: Optional list of middleware
|
||||
|
||||
Returns:
|
||||
A Starlette application with RequestContextMiddleware
|
||||
|
||||
|
||||
### `create_streamable_http_app` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L545" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
create_streamable_http_app(server: FastMCP[LifespanResultT], streamable_http_path: str, event_store: EventStore | None = None, retry_interval: int | None = None, auth: AuthProvider | None = None, json_response: bool = False, stateless_http: bool = False, debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None, host_origin_protection: HostOriginProtection = False, allowed_hosts: Sequence[str] | None = None, allowed_origins: Sequence[str] | None = None, session_idle_timeout: float | None = None) -> StarletteWithLifespan
|
||||
```
|
||||
|
||||
|
||||
Return an instance of the StreamableHTTP server app.
|
||||
|
||||
**Args:**
|
||||
- `server`: The FastMCP server instance
|
||||
- `streamable_http_path`: Path for StreamableHTTP connections
|
||||
- `event_store`: Optional event store for SSE polling/resumability
|
||||
- `retry_interval`: Optional retry interval in milliseconds for SSE polling.
|
||||
Controls how quickly clients should reconnect after server-initiated
|
||||
disconnections. Requires event_store to be set. Defaults to SDK default.
|
||||
- `auth`: Optional authentication provider (AuthProvider)
|
||||
- `json_response`: Whether to use JSON response format
|
||||
- `stateless_http`: Whether to use stateless mode (new transport per request)
|
||||
- `debug`: Whether to enable debug mode
|
||||
- `routes`: Optional list of custom routes
|
||||
- `middleware`: Optional list of middleware
|
||||
- `host_origin_protection`: Whether to validate Host and Origin headers
|
||||
before requests reach the MCP endpoint. Defaults to False for
|
||||
compatibility. "auto" protects localhost-bound servers and explicit
|
||||
host/origin allowlists.
|
||||
- `allowed_hosts`: Additional hostnames that may appear in the Host header.
|
||||
- `allowed_origins`: Additional browser origins trusted by the request guard.
|
||||
Configure CORS separately when browser JavaScript must read
|
||||
cross-origin responses.
|
||||
- `session_idle_timeout`: Maximum time in seconds a session may remain idle
|
||||
before it is terminated. The deadline is pushed forward on every
|
||||
request. When None, sessions never expire from inactivity. Not
|
||||
supported in stateless mode.
|
||||
|
||||
**Returns:**
|
||||
- A Starlette application with StreamableHTTP support
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `FastMCPStreamableHTTPSessionManager` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L43" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Session manager that scopes resumability storage per transport session.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `event_store` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L68" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
event_store(self) -> EventStore | None
|
||||
```
|
||||
|
||||
#### `event_store` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L76" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
event_store(self, event_store: EventStore | None) -> None
|
||||
```
|
||||
|
||||
### `StreamableHTTPASGIApp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L80" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
ASGI application wrapper for Streamable HTTP server transport.
|
||||
|
||||
|
||||
### `HostOriginGuardMiddleware` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L227" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Validate Host and Origin headers before requests reach MCP sessions.
|
||||
|
||||
|
||||
### `StarletteWithLifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L348" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L350" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
lifespan(self) -> Lifespan[Starlette]
|
||||
```
|
||||
|
||||
### `RequestContextMiddleware` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L363" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Middleware that stores each request in a ContextVar and sets transport type.
|
||||
|
||||
101
docs/python-sdk/fastmcp-server-lifespan.mdx
Normal file
101
docs/python-sdk/fastmcp-server-lifespan.mdx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
---
|
||||
title: lifespan
|
||||
sidebarTitle: lifespan
|
||||
---
|
||||
|
||||
# `fastmcp.server.lifespan`
|
||||
|
||||
|
||||
Composable lifespans for FastMCP servers.
|
||||
|
||||
This module provides a `@lifespan` decorator for creating composable server lifespans
|
||||
that can be combined using the `|` operator.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.lifespan import lifespan
|
||||
|
||||
@lifespan
|
||||
async def db_lifespan(server):
|
||||
conn = await connect_db()
|
||||
yield {"db": conn}
|
||||
await conn.close()
|
||||
|
||||
@lifespan
|
||||
async def cache_lifespan(server):
|
||||
cache = await connect_cache()
|
||||
yield {"cache": cache}
|
||||
await cache.close()
|
||||
|
||||
mcp = FastMCP("server", lifespan=db_lifespan | cache_lifespan)
|
||||
```
|
||||
|
||||
To compose with existing `@asynccontextmanager` lifespans, wrap them explicitly:
|
||||
|
||||
```python
|
||||
from contextlib import asynccontextmanager
|
||||
from fastmcp.server.lifespan import lifespan, ContextManagerLifespan
|
||||
|
||||
@asynccontextmanager
|
||||
async def legacy_lifespan(server):
|
||||
yield {"legacy": True}
|
||||
|
||||
@lifespan
|
||||
async def new_lifespan(server):
|
||||
yield {"new": True}
|
||||
|
||||
# Wrap the legacy lifespan explicitly
|
||||
combined = ContextManagerLifespan(legacy_lifespan) | new_lifespan
|
||||
```
|
||||
|
||||
|
||||
## Functions
|
||||
|
||||
### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/lifespan.py#L172" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
lifespan(fn: LifespanFn) -> Lifespan
|
||||
```
|
||||
|
||||
|
||||
Decorator to create a composable lifespan.
|
||||
|
||||
Use this decorator on an async generator function to make it composable
|
||||
with other lifespans using the `|` operator.
|
||||
|
||||
**Args:**
|
||||
- `fn`: An async generator function that takes a FastMCP server and yields
|
||||
a dict for the lifespan context.
|
||||
|
||||
**Returns:**
|
||||
- A composable Lifespan wrapper.
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `Lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/lifespan.py#L61" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Composable lifespan wrapper.
|
||||
|
||||
Wraps an async generator function and enables composition via the `|` operator.
|
||||
The wrapped function should yield a dict that becomes part of the lifespan context.
|
||||
|
||||
|
||||
### `ContextManagerLifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/lifespan.py#L110" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Lifespan wrapper for already-wrapped context manager functions.
|
||||
|
||||
Use this for functions already decorated with @asynccontextmanager.
|
||||
|
||||
|
||||
### `ComposedLifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/lifespan.py#L137" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Two lifespans composed together.
|
||||
|
||||
Enters the left lifespan first, then the right. Exits in reverse order.
|
||||
Results are shallow-merged into a single dict.
|
||||
|
||||
106
docs/python-sdk/fastmcp-server-low_level.mdx
Normal file
106
docs/python-sdk/fastmcp-server-low_level.mdx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
---
|
||||
title: low_level
|
||||
sidebarTitle: low_level
|
||||
---
|
||||
|
||||
# `fastmcp.server.low_level`
|
||||
|
||||
## Functions
|
||||
|
||||
### `client_supports_extension` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L111" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
client_supports_extension(session: ServerSession, extension_id: str) -> bool
|
||||
```
|
||||
|
||||
|
||||
Check whether the connected client supports a given MCP extension.
|
||||
|
||||
Inspects the ``extensions`` capability on ``ClientCapabilities`` sent by the
|
||||
client during initialization. In v2 the client's initialize params are
|
||||
reachable via ``session.client_params``.
|
||||
|
||||
SDK v2 declares ``extensions`` as a real field on ``ClientCapabilities``, so
|
||||
a client sending ``ClientCapabilities(extensions={...})`` populates the field
|
||||
directly. We read that field first and fall back to ``model_extra`` only for
|
||||
legacy-serialized clients that carried ``extensions`` as an extra key.
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `FastMCPServerMiddleware` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L140" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Root dispatch for the FastMCP middleware chain, in the SDK's middleware layer.
|
||||
|
||||
v2 no longer lets FastMCP subclass ``ServerSession`` (the runner constructs
|
||||
it per request), so the old ``MiddlewareServerSession._received_request``
|
||||
override is replaced by a ``ServerMiddleware`` — an ordinary entry in the
|
||||
SDK's own middleware list. Sitting at the root of dispatch, this
|
||||
is the single entry point through which *every* inbound message flows —
|
||||
requests, notifications, cancellations, ``initialize``, and even malformed or
|
||||
unroutable messages the SDK can still hand us. It binds the FastMCP
|
||||
request-context ContextVar and re-applies the app-scoped ``SharedContext`` for
|
||||
the whole chain, then runs the FastMCP ``Middleware`` chain so
|
||||
``on_message`` / ``on_request`` / ``on_notification`` observe the message.
|
||||
|
||||
Dispatch shapes:
|
||||
|
||||
- Negotiation runs the *whole* FastMCP chain here: ``initialize`` dispatches
|
||||
through ``on_initialize`` and ``server/discover`` through ``on_discover``.
|
||||
Neither has an interior FastMCP handler adapter, and the SDK serializes both
|
||||
results before returning through its middleware seam, so this root adapter
|
||||
restores core results to typed models before FastMCP middleware observes them.
|
||||
- The component methods (``tools/call``, ``tools/list``, ``resources/read``,
|
||||
...) still run their FastMCP chain *interior*, in the handler adapter, where
|
||||
``on_call_tool`` receives the typed component result and a tool exception
|
||||
propagates through ``on_message``/``on_request`` exactly where the built-in
|
||||
error/logging/timing middleware expect it. The root dispatch does not re-run the
|
||||
chain for these — it only steps in when such a request fails *before* the
|
||||
interior runs (malformed params, routing), so ``on_message`` still observes
|
||||
the failure.
|
||||
- Every other message — all notifications (including ``notifications/cancelled``
|
||||
and ``notifications/initialized``), ``ping``, ``logging/setLevel``, and any
|
||||
unroutable/non-component request — has no interior FastMCP dispatch, so the
|
||||
root dispatch runs the ``"outer"`` pass (``on_message`` plus
|
||||
``on_request``/``on_notification``) here, wrapping the real SDK dispatch.
|
||||
This closes the long-standing gap where these messages were invisible to
|
||||
FastMCP middleware.
|
||||
|
||||
|
||||
### `LowLevelServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L455" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `fastmcp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L507" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
fastmcp(self) -> FastMCP
|
||||
```
|
||||
|
||||
Get the FastMCP instance.
|
||||
|
||||
|
||||
#### `create_initialization_options` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L514" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
create_initialization_options(self, notification_options: NotificationOptions | None = None, experimental_capabilities: dict[str, dict[str, Any]] | None = None, extensions: dict[str, dict[str, Any]] | None = None) -> InitializationOptions
|
||||
```
|
||||
|
||||
#### `get_capabilities` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L529" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_capabilities(self, notification_options: NotificationOptions | None = None, experimental_capabilities: dict[str, dict[str, Any]] | None = None, extensions: dict[str, dict[str, Any]] | None = None) -> mcp_types.ServerCapabilities
|
||||
```
|
||||
|
||||
Override to advertise registered extensions and the MCP Apps UI extension.
|
||||
|
||||
``ServerCapabilities.extensions`` is a real declared field in v2, so we
|
||||
update it directly. The
|
||||
`FastMCP(experimental_capabilities=...)` merge also lives here rather
|
||||
than in `create_initialization_options`: the modern `server/discover`
|
||||
handler calls this directly, without going through
|
||||
`create_initialization_options` at all, so merging there only reached
|
||||
the handshake-era `initialize` response and silently dropped
|
||||
constructor-configured experimental capabilities from `discover`.
|
||||
|
||||
9
docs/python-sdk/fastmcp-server-mixins.mdx
Normal file
9
docs/python-sdk/fastmcp-server-mixins.mdx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
title: mixins
|
||||
sidebarTitle: mixins
|
||||
---
|
||||
|
||||
# `fastmcp.server.mixins`
|
||||
|
||||
|
||||
Server mixins for FastMCP.
|
||||
34
docs/python-sdk/fastmcp-server-providers.mdx
Normal file
34
docs/python-sdk/fastmcp-server-providers.mdx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
title: providers
|
||||
sidebarTitle: providers
|
||||
---
|
||||
|
||||
# `fastmcp.server.providers`
|
||||
|
||||
|
||||
Providers for dynamic MCP components.
|
||||
|
||||
This module provides the `Provider` abstraction for providing tools,
|
||||
resources, and prompts dynamically at runtime.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.providers import Provider
|
||||
from fastmcp.tools import Tool
|
||||
|
||||
class DatabaseProvider(Provider):
|
||||
def __init__(self, db_url: str):
|
||||
self.db = Database(db_url)
|
||||
|
||||
async def _list_tools(self) -> list[Tool]:
|
||||
rows = await self.db.fetch("SELECT * FROM tools")
|
||||
return [self._make_tool(row) for row in rows]
|
||||
|
||||
async def _get_tool(self, name: str) -> Tool | None:
|
||||
row = await self.db.fetchone("SELECT * FROM tools WHERE name = ?", name)
|
||||
return self._make_tool(row) if row else None
|
||||
|
||||
mcp = FastMCP("Server", providers=[DatabaseProvider(db_url)])
|
||||
```
|
||||
|
||||
891
docs/python-sdk/fastmcp-server-server.mdx
Normal file
891
docs/python-sdk/fastmcp-server-server.mdx
Normal file
|
|
@ -0,0 +1,891 @@
|
|||
---
|
||||
title: server
|
||||
sidebarTitle: server
|
||||
---
|
||||
|
||||
# `fastmcp.server.server`
|
||||
|
||||
|
||||
FastMCP - A more ergonomic interface for MCP servers.
|
||||
|
||||
## Functions
|
||||
|
||||
### `default_lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L237" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]
|
||||
```
|
||||
|
||||
|
||||
Default lifespan context manager that does nothing.
|
||||
|
||||
**Args:**
|
||||
- `server`: The server instance this lifespan is managing
|
||||
|
||||
**Returns:**
|
||||
- An empty dictionary as the lifespan result.
|
||||
|
||||
|
||||
### `create_proxy` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2501" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
create_proxy(target: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | SDKServer | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
|
||||
```
|
||||
|
||||
|
||||
Create a FastMCP proxy server for the given target.
|
||||
|
||||
This is the recommended way to create a proxy server. For lower-level control,
|
||||
use `FastMCPProxy` or `ProxyProvider` directly from `fastmcp.server.providers.proxy`.
|
||||
|
||||
**Args:**
|
||||
- `target`: The backend to proxy to. Can be\:
|
||||
- A Client instance (connected or disconnected)
|
||||
- A ClientTransport
|
||||
- A FastMCP server instance
|
||||
- A URL string or AnyUrl
|
||||
- A Path to a server script
|
||||
- An MCPConfig or dict
|
||||
- `mode`: Protocol-era negotiation for auto-created proxy clients (a
|
||||
non-Client target). By default (``None``) the backend MIRRORS the
|
||||
front connection's negotiated era per request, so the whole chain
|
||||
speaks one era end-to-end\: a modern front reaches a modern backend
|
||||
(a guard tool's `InputRequiredResult` (SEP-2322) round-trips) and a
|
||||
handshake front reaches a handshake backend (server-initiated
|
||||
sampling / elicitation / roots push-forwarding works). Pass an
|
||||
explicit mode (e.g. ``"auto"`` or a version string) to pin the
|
||||
backend era regardless of the front; this overrides mirroring and is
|
||||
appropriate when the backend only speaks one era. Ignored when
|
||||
`target` is already a `Client` (which carries its own mode).
|
||||
- `**settings`: Additional settings passed to FastMCPProxy (name, etc.)
|
||||
|
||||
**Returns:**
|
||||
- A FastMCPProxy server that proxies to the target.
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `StateValue` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L272" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L278" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `name` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L507" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
name(self) -> str
|
||||
```
|
||||
|
||||
#### `instructions` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L511" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
instructions(self) -> str | None
|
||||
```
|
||||
|
||||
#### `instructions` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L515" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L519" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L523" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L527" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
icons(self) -> list[mcp_types.Icon]
|
||||
```
|
||||
|
||||
#### `local_provider` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L534" 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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L598" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_middleware(self, middleware: Middleware) -> None
|
||||
```
|
||||
|
||||
#### `add_extension` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L601" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_extension(self, extension: ServerExtension) -> None
|
||||
```
|
||||
|
||||
Register a server extension (SEP-2133).
|
||||
|
||||
An extension contributes a negotiated capability, additive request
|
||||
methods, a `tools/call` interceptor, and an optional lifespan — each
|
||||
with access to FastMCP-level constructs (the component registry,
|
||||
`Context`, auth scope). Its capability is advertised only while it is
|
||||
registered.
|
||||
|
||||
The extension is bound to this server (so its handlers and interceptor
|
||||
can reach it), its method bindings are wired onto the low-level server,
|
||||
and it is recorded for capability advertisement, interception, and
|
||||
lifespan entry. Registering two extensions with the same identifier is
|
||||
an error, as is registering after the server's lifespan has started —
|
||||
the extension's lifespan could no longer run, leaving it silently
|
||||
half-active.
|
||||
|
||||
Extensions are served by the server they are registered on. A mounted
|
||||
child's extensions do not propagate to the root: the root serves the
|
||||
wire, so only root-registered extensions advertise capabilities and
|
||||
answer methods (matching the lifespan, which also defers to the root).
|
||||
Register extensions on the server you run.
|
||||
|
||||
|
||||
#### `add_provider` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L673" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_provider(self, provider: Provider) -> None
|
||||
```
|
||||
|
||||
Add a provider for dynamic tools, resources, and prompts.
|
||||
|
||||
Providers are queried in registration order. The first provider to return
|
||||
a non-None result wins. Static components (registered via decorators)
|
||||
always take precedence over providers.
|
||||
|
||||
**Args:**
|
||||
- `provider`: A Provider instance that will provide components dynamically.
|
||||
- `namespace`: Optional namespace prefix. When set\:
|
||||
- Tools become "namespace_toolname"
|
||||
- Resources become "protocol\://namespace/path"
|
||||
- Prompts become "namespace_promptname"
|
||||
|
||||
|
||||
#### `get_tasks` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L785" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_tasks(self) -> Sequence[FastMCPComponent]
|
||||
```
|
||||
|
||||
Get task-eligible components with all transforms applied.
|
||||
|
||||
Overrides AggregateProvider.get_tasks() to apply server-level transforms
|
||||
after aggregation. AggregateProvider handles provider-level namespacing.
|
||||
|
||||
|
||||
#### `add_transform` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L814" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_transform(self, transform: Transform) -> None
|
||||
```
|
||||
|
||||
Add a server-level transform.
|
||||
|
||||
Server-level transforms are applied after all providers are aggregated.
|
||||
They transform tools, resources, and prompts from ALL providers.
|
||||
|
||||
**Args:**
|
||||
- `transform`: The transform to add.
|
||||
|
||||
|
||||
#### `list_tools` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L834" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_tools(self) -> Sequence[Tool]
|
||||
```
|
||||
|
||||
List all enabled tools from providers.
|
||||
|
||||
Overrides Provider.list_tools() to add enabled filtering, auth filtering,
|
||||
and middleware execution. Returns all versions (no deduplication).
|
||||
Protocol handlers deduplicate for MCP wire format.
|
||||
|
||||
|
||||
#### `get_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L917" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_tool(self, name: str, version: VersionSpec | None = None) -> Tool | None
|
||||
```
|
||||
|
||||
Get a tool by name, filtering disabled tools.
|
||||
|
||||
Overrides Provider.get_tool() to filter disabled tools after all
|
||||
transforms (including session-level) have been applied. This ensures
|
||||
session transforms can override provider-level disables.
|
||||
|
||||
When the highest version is disabled and no explicit version was
|
||||
requested, falls back to the next-highest enabled version.
|
||||
|
||||
**Args:**
|
||||
- `name`: The tool name.
|
||||
- `version`: Version filter (None returns highest version).
|
||||
|
||||
**Returns:**
|
||||
- The tool if found and enabled, None otherwise.
|
||||
|
||||
|
||||
#### `list_resources` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L971" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_resources(self) -> Sequence[Resource]
|
||||
```
|
||||
|
||||
List all enabled resources from providers.
|
||||
|
||||
Overrides Provider.list_resources() to add visibility filtering, auth filtering,
|
||||
and middleware execution. Returns all versions (no deduplication).
|
||||
Protocol handlers deduplicate for MCP wire format.
|
||||
|
||||
|
||||
#### `get_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1056" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_resource(self, uri: str, version: VersionSpec | None = None) -> Resource | None
|
||||
```
|
||||
|
||||
Get a resource by URI, filtering disabled resources.
|
||||
|
||||
Overrides Provider.get_resource() to add visibility filtering after all
|
||||
transforms (including session-level) have been applied.
|
||||
|
||||
When the highest version is disabled and no explicit version was
|
||||
requested, falls back to the next-highest enabled version.
|
||||
|
||||
**Args:**
|
||||
- `uri`: The resource URI.
|
||||
- `version`: Version filter (None returns highest version).
|
||||
|
||||
**Returns:**
|
||||
- The resource if found and enabled, None otherwise.
|
||||
|
||||
|
||||
#### `list_resource_templates` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_resource_templates(self) -> Sequence[ResourceTemplate]
|
||||
```
|
||||
|
||||
List all enabled resource templates from providers.
|
||||
|
||||
Overrides Provider.list_resource_templates() to add visibility filtering,
|
||||
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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1188" 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
|
||||
```
|
||||
|
||||
Get a resource template by URI, filtering disabled templates.
|
||||
|
||||
Overrides Provider.get_resource_template() to add visibility filtering after
|
||||
all transforms (including session-level) have been applied.
|
||||
|
||||
When the highest version is disabled and no explicit version was
|
||||
requested, falls back to the next-highest enabled version.
|
||||
|
||||
**Args:**
|
||||
- `uri`: The template URI.
|
||||
- `version`: Version filter (None returns highest version).
|
||||
|
||||
**Returns:**
|
||||
- The template if found and enabled, None otherwise.
|
||||
|
||||
|
||||
#### `list_prompts` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1242" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_prompts(self) -> Sequence[Prompt]
|
||||
```
|
||||
|
||||
List all enabled prompts from providers.
|
||||
|
||||
Overrides Provider.list_prompts() to add visibility filtering, auth filtering,
|
||||
and middleware execution. Returns all versions (no deduplication).
|
||||
Protocol handlers deduplicate for MCP wire format.
|
||||
|
||||
|
||||
#### `get_prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1314" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_prompt(self, name: str, version: VersionSpec | None = None) -> Prompt | None
|
||||
```
|
||||
|
||||
Get a prompt by name, filtering disabled prompts.
|
||||
|
||||
Overrides Provider.get_prompt() to add visibility filtering after all
|
||||
transforms (including session-level) have been applied.
|
||||
|
||||
When the highest version is disabled and no explicit version was
|
||||
requested, falls back to the next-highest enabled version.
|
||||
|
||||
**Args:**
|
||||
- `name`: The prompt name.
|
||||
- `version`: Version filter (None returns highest version).
|
||||
|
||||
**Returns:**
|
||||
- The prompt if found and enabled, None otherwise.
|
||||
|
||||
|
||||
#### `call_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1364" 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 a tool by name.
|
||||
|
||||
This is the public API for executing tools. By default, middleware is applied.
|
||||
|
||||
**Args:**
|
||||
- `name`: The tool name
|
||||
- `arguments`: Tool arguments (optional)
|
||||
- `version`: Specific version to call. If None, calls highest version.
|
||||
- `run_middleware`: If True (default), apply the middleware chain.
|
||||
Set to False when called from middleware to avoid re-applying.
|
||||
|
||||
**Returns:**
|
||||
- ToolResult.
|
||||
|
||||
A guard tool that requests client input (SEP-2322 multi-round-trip)
|
||||
returns an ``InputRequiredToolResult`` (a ``ToolResult`` subclass); it
|
||||
flows back through the middleware chain as an ordinary result and the
|
||||
wire handler unwraps it into an ``InputRequiredResult`` on the response.
|
||||
|
||||
**Raises:**
|
||||
- `NotFoundError`: If tool not found or disabled
|
||||
- `ToolError`: If tool execution fails
|
||||
- `ValidationError`: If arguments fail validation
|
||||
|
||||
|
||||
#### `read_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1557" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
read_resource(self, uri: str) -> ResourceResult
|
||||
```
|
||||
|
||||
Read a resource by URI.
|
||||
|
||||
This is the public API for reading resources. By default, middleware is applied.
|
||||
Checks concrete resources first, then templates.
|
||||
|
||||
**Args:**
|
||||
- `uri`: The resource URI
|
||||
- `version`: Specific version to read. If None, reads highest version.
|
||||
- `run_middleware`: If True (default), apply the middleware chain.
|
||||
Set to False when called from middleware to avoid re-applying.
|
||||
|
||||
**Returns:**
|
||||
- ResourceResult.
|
||||
|
||||
**Raises:**
|
||||
- `NotFoundError`: If resource not found or disabled
|
||||
- `ResourceError`: If resource read fails
|
||||
|
||||
|
||||
#### `render_prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1715" 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 a prompt by name.
|
||||
|
||||
This is the public API for rendering prompts. By default, middleware is applied.
|
||||
Use get_prompt() to retrieve the prompt definition without rendering.
|
||||
|
||||
**Args:**
|
||||
- `name`: The prompt name
|
||||
- `arguments`: Prompt arguments (optional)
|
||||
- `version`: Specific version to render. If None, renders highest version.
|
||||
- `run_middleware`: If True (default), apply the middleware chain.
|
||||
Set to False when called from middleware to avoid re-applying.
|
||||
|
||||
**Returns:**
|
||||
- PromptResult.
|
||||
|
||||
**Raises:**
|
||||
- `NotFoundError`: If prompt not found or disabled
|
||||
- `PromptError`: If prompt rendering fails
|
||||
|
||||
|
||||
#### `add_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1795" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_tool(self, tool: Tool | Callable[..., Any]) -> Tool
|
||||
```
|
||||
|
||||
Add a tool to the server.
|
||||
|
||||
The tool function can optionally request a Context object by adding a parameter
|
||||
with the Context type annotation. See the @tool decorator for examples.
|
||||
|
||||
**Args:**
|
||||
- `tool`: The Tool instance or @tool-decorated function to register
|
||||
|
||||
**Returns:**
|
||||
- The tool instance that was added to the server.
|
||||
|
||||
|
||||
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1810" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
tool(self, name_or_fn: F) -> F
|
||||
```
|
||||
|
||||
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1831" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
tool(self, name_or_fn: str | None = None) -> Callable[[F], F]
|
||||
```
|
||||
|
||||
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1851" 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]
|
||||
```
|
||||
|
||||
Decorator to register a tool.
|
||||
|
||||
Tools can optionally request a Context object by adding a parameter with the
|
||||
Context type annotation. The context provides access to MCP capabilities like
|
||||
logging, progress reporting, and resource access.
|
||||
|
||||
This decorator supports multiple calling patterns:
|
||||
- @server.tool (without parentheses)
|
||||
- @server.tool (with empty parentheses)
|
||||
- @server.tool("custom_name") (with name as first argument)
|
||||
- @server.tool(name="custom_name") (with name as keyword argument)
|
||||
- server.tool(function, name="custom_name") (direct function call)
|
||||
|
||||
**Args:**
|
||||
- `name_or_fn`: Either a function (when used as @tool), a string name, or None
|
||||
- `name`: Optional name for the tool (keyword-only, alternative to name_or_fn)
|
||||
- `description`: Optional description of what the tool does
|
||||
- `tags`: Optional set of tags for categorizing the tool
|
||||
- `output_schema`: Optional JSON schema for the tool's output
|
||||
- `annotations`: Optional annotations about the tool's behavior
|
||||
- `meta`: Optional meta information about the tool
|
||||
|
||||
**Examples:**
|
||||
|
||||
Register a tool with a custom name:
|
||||
```python
|
||||
@server.tool
|
||||
def my_tool(x: int) -> str:
|
||||
return str(x)
|
||||
|
||||
# Register a tool with a custom name
|
||||
@server.tool
|
||||
def my_tool(x: int) -> str:
|
||||
return str(x)
|
||||
|
||||
@server.tool("custom_name")
|
||||
def my_tool(x: int) -> str:
|
||||
return str(x)
|
||||
|
||||
@server.tool(name="custom_name")
|
||||
def my_tool(x: int) -> str:
|
||||
return str(x)
|
||||
|
||||
# Direct function call
|
||||
server.tool(my_function, name="custom_name")
|
||||
```
|
||||
|
||||
|
||||
#### `add_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1948" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_resource(self, resource: Resource | Callable[..., Any]) -> Resource | ResourceTemplate
|
||||
```
|
||||
|
||||
Add a resource to the server.
|
||||
|
||||
**Args:**
|
||||
- `resource`: A Resource instance or @resource-decorated function to add
|
||||
|
||||
**Returns:**
|
||||
- The resource instance that was added to the server.
|
||||
|
||||
|
||||
#### `add_template` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1961" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_template(self, template: ResourceTemplate) -> ResourceTemplate
|
||||
```
|
||||
|
||||
Add a resource template to the server.
|
||||
|
||||
**Args:**
|
||||
- `template`: A ResourceTemplate instance to add
|
||||
|
||||
**Returns:**
|
||||
- The template instance that was added to the server.
|
||||
|
||||
|
||||
#### `resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1972" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
resource(self, uri: str) -> Callable[[F], F]
|
||||
```
|
||||
|
||||
Decorator to register a function as a resource.
|
||||
|
||||
The function will be called when the resource is read to generate its content.
|
||||
The function can return:
|
||||
- str for text content
|
||||
- bytes for binary content
|
||||
- other types will be converted to JSON
|
||||
|
||||
Resources can optionally request a Context object by adding a parameter with the
|
||||
Context type annotation. The context provides access to MCP capabilities like
|
||||
logging, progress reporting, and session information.
|
||||
|
||||
If the URI contains parameters (e.g. "resource://{param}") or the function
|
||||
has parameters, it will be registered as a template resource.
|
||||
|
||||
**Args:**
|
||||
- `uri`: URI for the resource (e.g. "resource\://my-resource" or "resource\://{param}")
|
||||
- `name`: Optional name for the resource
|
||||
- `description`: Optional description of the resource
|
||||
- `mime_type`: Optional MIME type for the resource
|
||||
- `tags`: Optional set of tags for categorizing the resource
|
||||
- `annotations`: Optional annotations about the resource's behavior
|
||||
- `meta`: Optional meta information about the resource
|
||||
|
||||
**Examples:**
|
||||
|
||||
Register a resource with a custom name:
|
||||
```python
|
||||
@server.resource("resource://my-resource")
|
||||
def get_data() -> str:
|
||||
return "Hello, world!"
|
||||
|
||||
@server.resource("resource://my-resource")
|
||||
async get_data() -> str:
|
||||
data = await fetch_data()
|
||||
return f"Hello, world! {data}"
|
||||
|
||||
@server.resource("resource://{city}/weather")
|
||||
def get_weather(city: str) -> str:
|
||||
return f"Weather for {city}"
|
||||
|
||||
@server.resource("resource://{city}/weather")
|
||||
async def get_weather_with_context(city: str, ctx: Context) -> str:
|
||||
await ctx.info(f"Fetching weather for {city}")
|
||||
return f"Weather for {city}"
|
||||
|
||||
@server.resource("resource://{city}/weather")
|
||||
async def get_weather(city: str) -> str:
|
||||
data = await fetch_weather(city)
|
||||
return f"Weather for {city}: {data}"
|
||||
```
|
||||
|
||||
|
||||
#### `add_prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2091" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_prompt(self, prompt: Prompt | Callable[..., Any]) -> Prompt
|
||||
```
|
||||
|
||||
Add a prompt to the server.
|
||||
|
||||
**Args:**
|
||||
- `prompt`: A Prompt instance or @prompt-decorated function to add
|
||||
|
||||
**Returns:**
|
||||
- The prompt instance that was added to the server.
|
||||
|
||||
|
||||
#### `prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2103" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
prompt(self, name_or_fn: F) -> F
|
||||
```
|
||||
|
||||
#### `prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2118" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
prompt(self, name_or_fn: str | None = None) -> Callable[[F], F]
|
||||
```
|
||||
|
||||
#### `prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2132" 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]
|
||||
```
|
||||
|
||||
Decorator to register a prompt.
|
||||
|
||||
Prompts can optionally request a Context object by adding a parameter with the
|
||||
Context type annotation. The context provides access to MCP capabilities like
|
||||
logging, progress reporting, and session information.
|
||||
|
||||
This decorator supports multiple calling patterns:
|
||||
- @server.prompt (without parentheses)
|
||||
- @server.prompt() (with empty parentheses)
|
||||
- @server.prompt("custom_name") (with name as first argument)
|
||||
- @server.prompt(name="custom_name") (with name as keyword argument)
|
||||
- server.prompt(function, name="custom_name") (direct function call)
|
||||
|
||||
Args:
|
||||
name_or_fn: Either a function (when used as @prompt), a string name, or None
|
||||
name: Optional name for the prompt (keyword-only, alternative to name_or_fn)
|
||||
description: Optional description of what the prompt does
|
||||
tags: Optional set of tags for categorizing the prompt
|
||||
meta: Optional meta information about the prompt
|
||||
|
||||
Examples:
|
||||
|
||||
```python
|
||||
@server.prompt
|
||||
def analyze_table(table_name: str) -> list[Message]:
|
||||
schema = read_table_schema(table_name)
|
||||
return [
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Analyze this schema:
|
||||
{schema}"
|
||||
}
|
||||
]
|
||||
|
||||
@server.prompt()
|
||||
async def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:
|
||||
await ctx.info(f"Analyzing table {table_name}")
|
||||
schema = read_table_schema(table_name)
|
||||
return [
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Analyze this schema:
|
||||
{schema}"
|
||||
}
|
||||
]
|
||||
|
||||
@server.prompt("custom_name")
|
||||
async def analyze_file(path: str) -> list[Message]:
|
||||
content = await read_file(path)
|
||||
return [
|
||||
{
|
||||
"role": "user",
|
||||
"content": {
|
||||
"type": "resource",
|
||||
"resource": {
|
||||
"uri": f"file://{path}",
|
||||
"text": content
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@server.prompt(name="custom_name")
|
||||
def another_prompt(data: str) -> list[Message]:
|
||||
return [{"role": "user", "content": data}]
|
||||
|
||||
# Direct function call
|
||||
server.prompt(my_function, name="custom_name")
|
||||
```
|
||||
|
||||
|
||||
#### `add_completion_handler` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2230" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
add_completion_handler(self, handler: CompletionHandler) -> None
|
||||
```
|
||||
|
||||
Register the server's argument-completion handler.
|
||||
|
||||
A server has a single completion handler that answers every
|
||||
`completion/complete` request, switching on the reference (a prompt or
|
||||
resource template) and the argument being completed. Registering it also
|
||||
registers the low-level `completion/complete` handler, which is what
|
||||
makes the SDK declare the completions capability — so the capability is
|
||||
advertised exactly when the server can answer. Calling this again
|
||||
replaces the handler.
|
||||
|
||||
**Args:**
|
||||
- `handler`: A callable taking the reference, the
|
||||
`CompletionArgument`, and the optional `CompletionContext`, and
|
||||
returning candidate values (a `Completion`, a list of strings,
|
||||
or None). May be sync or async.
|
||||
|
||||
|
||||
#### `completion` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2251" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
completion(self, handler: CompletionHandler) -> CompletionHandler
|
||||
```
|
||||
|
||||
#### `completion` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2254" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
completion(self) -> Callable[[CompletionHandler], CompletionHandler]
|
||||
```
|
||||
|
||||
#### `completion` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2258" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
completion(self, handler: CompletionHandler | None = None) -> CompletionHandler | Callable[[CompletionHandler], CompletionHandler]
|
||||
```
|
||||
|
||||
Decorator to register the server's argument-completion handler.
|
||||
|
||||
The handler answers `completion/complete` requests for prompt arguments
|
||||
and resource-template parameters. It receives the reference being
|
||||
completed, the argument (its name and the partial value typed so far),
|
||||
and the context of arguments already supplied, and returns candidate
|
||||
values. Return a list of strings, a `Completion` (to include pagination
|
||||
hints), or None when the reference/argument is not one it handles — an
|
||||
unhandled reference yields an empty completion, not an error.
|
||||
|
||||
Registering a handler declares the completions capability; a server with
|
||||
none does not advertise it. This works identically on the handshake and
|
||||
modern protocol eras.
|
||||
|
||||
Supports both `@mcp.completion` and `@mcp.completion()`.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from mcp_types import Completion, PromptReference
|
||||
|
||||
mcp = FastMCP("Completion Server")
|
||||
|
||||
@mcp.prompt
|
||||
def poem(theme: str) -> str:
|
||||
return f"Write a poem about {theme}"
|
||||
|
||||
@mcp.completion
|
||||
def complete(ref, argument, context):
|
||||
if isinstance(ref, PromptReference) and ref.name == "poem":
|
||||
if argument.name == "theme":
|
||||
options = ["nature", "love", "adventure"]
|
||||
return [o for o in options if o.startswith(argument.value)]
|
||||
return None
|
||||
```
|
||||
|
||||
|
||||
#### `mount` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2308" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
mount(self, server: FastMCP[LifespanResultT], namespace: str | None = None, tool_names: dict[str, str] | None = None) -> None
|
||||
```
|
||||
|
||||
Mount another FastMCP server on this server with an optional namespace.
|
||||
|
||||
Mounting establishes a dynamic connection between servers. When a client
|
||||
interacts with a mounted server's objects through the parent server, requests
|
||||
are forwarded to the mounted server in real-time. This means changes to the
|
||||
mounted server are immediately reflected when accessed through the parent.
|
||||
|
||||
When a server is mounted with a namespace:
|
||||
- Tools from the mounted server are accessible with namespaced names.
|
||||
Example: If server has a tool named "get_weather", it will be available as "namespace_get_weather".
|
||||
- Resources are accessible with namespaced URIs.
|
||||
Example: If server has a resource with URI "weather://forecast", it will be available as
|
||||
"weather://namespace/forecast".
|
||||
- Templates are accessible with namespaced URI templates.
|
||||
Example: If server has a template with URI "weather://location/{id}", it will be available
|
||||
as "weather://namespace/location/{id}".
|
||||
- Prompts are accessible with namespaced names.
|
||||
Example: If server has a prompt named "weather_prompt", it will be available as
|
||||
"namespace_weather_prompt".
|
||||
|
||||
When a server is mounted without a namespace (namespace=None), its tools, resources, templates,
|
||||
and prompts are accessible with their original names. Multiple servers can be mounted
|
||||
without namespaces, and they will be tried in order until a match is found.
|
||||
|
||||
The mounted server's lifespan is executed when the parent server starts, and its
|
||||
middleware chain is invoked for all operations (tool calls, resource reads, prompts).
|
||||
|
||||
**Args:**
|
||||
- `server`: The FastMCP server to mount.
|
||||
- `namespace`: Optional namespace to use for the mounted server's objects. If None,
|
||||
the server's objects are accessible with their original names.
|
||||
- `tool_names`: Optional mapping of original tool names to custom names. Use this
|
||||
to override namespaced names. Keys are the original tool names from the
|
||||
mounted server.
|
||||
|
||||
|
||||
#### `from_openapi` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2379" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
from_openapi(cls, openapi_spec: dict[str, Any], client: httpx2.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.
|
||||
|
||||
**Args:**
|
||||
- `openapi_spec`: OpenAPI schema as a dictionary
|
||||
- `client`: Optional httpx2 AsyncClient for making HTTP requests.
|
||||
If not provided, a default client is created using the first
|
||||
server URL from the OpenAPI spec with a 30-second timeout.
|
||||
Legacy httpx clients are temporarily accepted with a deprecation
|
||||
warning.
|
||||
- `name`: Name for the MCP server
|
||||
- `route_maps`: Optional list of RouteMap objects defining route mappings
|
||||
- `route_map_fn`: Optional callable for advanced route type mapping
|
||||
- `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/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2432" 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
|
||||
```
|
||||
|
||||
Create a FastMCP server from a FastAPI application.
|
||||
|
||||
**Args:**
|
||||
- `app`: FastAPI application instance
|
||||
- `name`: Name for the MCP server (defaults to app.title)
|
||||
- `route_maps`: Optional list of RouteMap objects defining route mappings
|
||||
- `route_map_fn`: Optional callable for advanced route type mapping
|
||||
- `mcp_component_fn`: Optional callable for component customization
|
||||
- `mcp_names`: Optional dictionary mapping operationId to component names
|
||||
- `httpx_client_kwargs`: Optional kwargs passed to httpx2.AsyncClient.
|
||||
Use this to configure timeout and other client settings.
|
||||
- `tags`: Optional set of tags to add to all components
|
||||
- `**settings`: Additional settings passed to FastMCP
|
||||
|
||||
**Returns:**
|
||||
- A FastMCP server with an OpenAPIProvider attached.
|
||||
|
||||
|
||||
#### `generate_name` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2487" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
generate_name(cls, name: str | None = None) -> str
|
||||
```
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
title: session_scoped_event_store
|
||||
sidebarTitle: session_scoped_event_store
|
||||
---
|
||||
|
||||
# `fastmcp.server.session_scoped_event_store`
|
||||
|
||||
|
||||
Lightweight session scoping for Streamable HTTP event stores.
|
||||
|
||||
## Classes
|
||||
|
||||
### `SessionScopedEventStore` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/session_scoped_event_store.py#L19" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
EventStore adapter that isolates stream IDs to one transport session.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `store_event` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/session_scoped_event_store.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId
|
||||
```
|
||||
|
||||
#### `replay_events_after` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/session_scoped_event_store.py#L41" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
replay_events_after(self, last_event_id: EventId, send_callback: EventCallback) -> StreamId | None
|
||||
```
|
||||
319
docs/python-sdk/fastmcp-server-sessions.mdx
Normal file
319
docs/python-sdk/fastmcp-server-sessions.mdx
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
---
|
||||
title: sessions
|
||||
sidebarTitle: sessions
|
||||
---
|
||||
|
||||
# `fastmcp.server.sessions`
|
||||
|
||||
|
||||
Stateless session state: server-side per-user and per-session storage.
|
||||
|
||||
Modern (2026-07-28) MCP connections are stateless by construction — every
|
||||
request builds a fresh connection whose in-memory state is discarded when the
|
||||
request returns. This module gives tools two explicit ways to keep state across
|
||||
calls, both backed by the server's existing state store and both isolated by the
|
||||
authenticated principal rather than by any client-declared identifier.
|
||||
|
||||
- `Session`: async `get`/`set`/`delete`/`clear` over a single dict stored under
|
||||
one key, scoped to a `(principal, session_id)` pair. This is the state-accessor
|
||||
object a handler works with — the value the standalone `get_session(id)`
|
||||
returns and the value injected for a `UserSession` parameter.
|
||||
- `session: UserSession` (injected): a per-user bucket, dependency-injected like
|
||||
`ctx: Context` and keyed by the request's authenticated principal. Requires
|
||||
auth. `UserSession` is the injection annotation; the injected value is a
|
||||
`Session`. It is always available under auth — no `create_session`, no
|
||||
provider, no validation.
|
||||
- `session_id: SessionId` (argument): a required string the agent supplies,
|
||||
resolved with the standalone `await get_session(session_id)`. The id is
|
||||
minted
|
||||
by `create_session`; an id that was never created (or was created under a
|
||||
different principal) is rejected. This validation is the whole guarantee — an
|
||||
unminted id never resolves, so nothing enforces provider registration.
|
||||
- `SessionProvider`: a `Provider` contributing `create_session` / `end_session`
|
||||
tools. Register it with `mcp.add_provider(SessionProvider())` so a tool that
|
||||
takes `session_id` has a way to mint ids; without it, no id can be created, so
|
||||
those tools simply cannot resolve a session.
|
||||
|
||||
Isolation is the authenticated principal, not the session id. State keyed by
|
||||
`(principal, session_id)` means a request under principal B can never address
|
||||
principal A's keys, no matter what `session_id` it passes; the id only organizes
|
||||
sessions within a principal. Without auth there is no principal wall — a session
|
||||
id is a bearer capability and sessions are not a boundary between clients.
|
||||
|
||||
|
||||
## Functions
|
||||
|
||||
### `current_principal` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L139" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
current_principal() -> str | None
|
||||
```
|
||||
|
||||
|
||||
The authenticated principal for the current request as a compact JSON string.
|
||||
|
||||
Returns the `(client_id, issuer, subject)` triple encoded as compact JSON, or
|
||||
`None` on an unauthenticated request. Two users of one OAuth client are
|
||||
distinct principals whenever the token verifier supplies a subject.
|
||||
|
||||
|
||||
### `session_storage_key` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L164" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
session_storage_key(principal: str | None, session_id: str) -> str
|
||||
```
|
||||
|
||||
|
||||
The single storage key holding a session's state dict.
|
||||
|
||||
Keyed by `(principal, session_id)`: the principal is the isolation wall, the
|
||||
id organizes sessions within it. A session's whole state lives under this one
|
||||
key as a dict, so one key means one store TTL per session and `end` is a
|
||||
single delete.
|
||||
|
||||
|
||||
### `session_id_parameter_names` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L343" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
session_id_parameter_names(fn: Callable[..., object]) -> tuple[str, ...]
|
||||
```
|
||||
|
||||
|
||||
Names of a function's parameters annotated with `SessionId`.
|
||||
|
||||
Scans resolved type hints for `Annotated[str, _SessionIdMarker()]` metadata.
|
||||
Returns an empty tuple when the hints cannot be resolved (the function then
|
||||
simply carries no auto-populated session-id description).
|
||||
|
||||
`functools.partial` is unwrapped first, since `get_type_hints` rejects a
|
||||
partial object — FastMCP supports registering a partial as a tool, and its
|
||||
schema is still built from the underlying function, so its `SessionId`
|
||||
parameters must be detected here too. Parameters the partial has already
|
||||
bound — positionally or by keyword — are dropped, matching the tool's actual
|
||||
argument surface (the partial's own signature already reflects this).
|
||||
|
||||
|
||||
### `CurrentSession` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L449" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
CurrentSession() -> Session
|
||||
```
|
||||
|
||||
|
||||
Inject the per-user `Session` for the current authenticated principal.
|
||||
|
||||
Rarely written explicitly — a `session: UserSession` parameter is rewritten
|
||||
to this. Provided for parity with `CurrentContext()` when an explicit default
|
||||
is preferred.
|
||||
|
||||
|
||||
### `OptionalCurrentSession` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L459" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
OptionalCurrentSession() -> Session | None
|
||||
```
|
||||
|
||||
|
||||
Inject the per-user `Session`, or `None` when the request is unauthenticated.
|
||||
|
||||
Rarely written explicitly — a `session: UserSession | None = None` parameter
|
||||
is rewritten to this. Provided for parity with `OptionalCurrentContext()`.
|
||||
|
||||
|
||||
### `create_session` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L468" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
create_session() -> str
|
||||
```
|
||||
|
||||
|
||||
Create a new session and return its identifier.
|
||||
|
||||
Mints an unguessable `uuid4`, records an initial session owned by the current
|
||||
principal, and returns the id as a string. Store it and pass it back as a
|
||||
`session_id` argument on later calls to persist state across a session — only
|
||||
an id created this way resolves. State is keyed by the authenticated
|
||||
principal, so the id organizes sessions within a user; on an unauthenticated
|
||||
connection the id is the only thing standing between callers, which is why it
|
||||
is unguessable.
|
||||
|
||||
|
||||
### `end_session` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L490" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
end_session(session_id: SessionId) -> str
|
||||
```
|
||||
|
||||
|
||||
End a session and delete all of its state.
|
||||
|
||||
Validates the id like any other resolution (an unknown or foreign id is
|
||||
rejected), then deletes the session's key so the id no longer resolves.
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `SessionAuthError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
An injected `session: UserSession` was requested with no authenticated principal.
|
||||
|
||||
Per-user session injection keys off the request's authenticated principal, so
|
||||
it is only meaningful under auth. A tool that needs cross-call state without
|
||||
auth should take a `session_id: SessionId` argument instead.
|
||||
|
||||
|
||||
### `InvalidSession` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L125" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
A session id did not resolve to a session created under the current principal.
|
||||
|
||||
Raised by `get_session(session_id)` when the id was never created, or was
|
||||
created under a different principal. The public message is deliberately
|
||||
generic — the specific reason (which id, which principal) is logged at debug
|
||||
level, not returned to the caller, so an attacker cannot distinguish "unknown
|
||||
id" from "belongs to someone else".
|
||||
|
||||
|
||||
### `Session` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L175" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Async accessors over one `(principal, session_id)` bucket of state.
|
||||
|
||||
A session's state is a single dict stored under one key. That dict holds user
|
||||
state in a `state` sub-dict and a small creation marker alongside it, so a
|
||||
created-but-empty session is still distinguishable from a missing one.
|
||||
`get`/`set`/`delete` read-modify-write the sub-dict; `clear` empties the
|
||||
sub-dict but keeps the session valid; `end` deletes the whole key. Writes
|
||||
never impose a TTL — retention is entirely the server store's (configure it on
|
||||
the store you pass to `FastMCP(session_state_store=...)`).
|
||||
|
||||
Concurrent writes to one session race on the read-modify-write; session state
|
||||
is small and typically driven serially by one agent, so this is acceptable.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L205" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
id(self) -> str | None
|
||||
```
|
||||
|
||||
The session's identifier, or `None` for an injected per-user session.
|
||||
|
||||
For a session resolved from a `session_id` argument (or minted by
|
||||
`create_session`) this is that id. An injected `UserSession` has no
|
||||
distinct id — its bucket is the authenticated user — so it is `None`; the
|
||||
internal principal-derived key is deliberately not exposed here.
|
||||
|
||||
|
||||
#### `get` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L254" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get(self, key: str, default: Any = None) -> Any
|
||||
```
|
||||
|
||||
Return the value for `key`, or `default` when it is not set.
|
||||
|
||||
|
||||
#### `set` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L259" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set(self, key: str, value: Any) -> None
|
||||
```
|
||||
|
||||
Store `value` under `key` in this session (read-modify-write).
|
||||
|
||||
Preserves the creation marker: only the user-state sub-dict is touched.
|
||||
|
||||
|
||||
#### `delete` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L270" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
delete(self, key: str) -> None
|
||||
```
|
||||
|
||||
Remove `key` from this session, if present (preserves the marker).
|
||||
|
||||
|
||||
#### `clear` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L281" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
clear(self) -> None
|
||||
```
|
||||
|
||||
Empty the session's user state but keep the session valid.
|
||||
|
||||
The user-state sub-dict is reset to empty while the creation marker stays
|
||||
in place, so a cleared session still resolves through `get_session`.
|
||||
To invalidate a session entirely, use `end` (what `end_session` calls).
|
||||
|
||||
|
||||
#### `end` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L294" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
end(self) -> None
|
||||
```
|
||||
|
||||
Invalidate the session — delete its one key and all of its state.
|
||||
|
||||
After this the id no longer resolves through `get_session`. This is
|
||||
what `end_session` calls; `clear` only empties state and keeps the session.
|
||||
|
||||
|
||||
### `UserSession` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L303" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Annotation marker for the injected per-user session.
|
||||
|
||||
A `session: UserSession` parameter is **dependency-injected** like
|
||||
`ctx: Context`: keyed by the request's authenticated principal, excluded from
|
||||
the input schema, and requiring auth (it raises `SessionAuthError` with no
|
||||
principal). It doubles as the injection *annotation* and the injected
|
||||
type — the value a handler receives is a `UserSession`, which subclasses
|
||||
`Session`, so `await session.get(...)`, `.set`, `.delete`, and `.clear` all
|
||||
work exactly as on any other `Session`.
|
||||
|
||||
Unlike `session_id: SessionId`, the per-user bucket needs no `create_session`,
|
||||
no `SessionProvider`, and no validation — it is always available under auth,
|
||||
keyed directly by the caller's identity.
|
||||
|
||||
```python
|
||||
from fastmcp.server.sessions import UserSession
|
||||
|
||||
@mcp.tool
|
||||
async def remember(fact: str, session: UserSession) -> str:
|
||||
await session.set("fact", fact)
|
||||
return "noted"
|
||||
```
|
||||
|
||||
Subclasses `Session` only so the framework's type-based injection detector can
|
||||
key off it; it adds no behavior of its own.
|
||||
|
||||
|
||||
### `SessionProvider` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/sessions.py#L501" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Provider contributing the session lifecycle tools.
|
||||
|
||||
Register it whenever a tool declares a `session_id: SessionId` argument:
|
||||
|
||||
```python
|
||||
from fastmcp.server.sessions import SessionProvider
|
||||
|
||||
mcp.add_provider(SessionProvider())
|
||||
```
|
||||
|
||||
It registers two tools:
|
||||
|
||||
- `create_session()` mints an unguessable `uuid4`, records the session, and
|
||||
returns the id.
|
||||
- `end_session(session_id)` invalidates that session and deletes its state.
|
||||
|
||||
It owns no storage (session state lives in the server's configured
|
||||
`session_state_store`) and imposes no TTL (retention is the store's). It
|
||||
exists to mint and end owned session ids. Registration is not enforced: with
|
||||
no provider, no id can be created, so every `get_session(...)` rejects —
|
||||
a `session_id` tool without a provider simply cannot resolve a session.
|
||||
|
||||
117
docs/python-sdk/fastmcp-server-telemetry.mdx
Normal file
117
docs/python-sdk/fastmcp-server-telemetry.mdx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
---
|
||||
title: telemetry
|
||||
sidebarTitle: telemetry
|
||||
---
|
||||
|
||||
# `fastmcp.server.telemetry`
|
||||
|
||||
|
||||
Server-side telemetry helpers.
|
||||
|
||||
## Functions
|
||||
|
||||
### `get_auth_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/telemetry.py#L43" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_auth_span_attributes() -> dict[str, str]
|
||||
```
|
||||
|
||||
|
||||
Get auth attributes for the current request, if authenticated.
|
||||
|
||||
|
||||
### `get_session_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/telemetry.py#L60" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_session_span_attributes() -> dict[str, str]
|
||||
```
|
||||
|
||||
|
||||
Get session attributes for the current request.
|
||||
|
||||
|
||||
### `get_protocol_span_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/telemetry.py#L74" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_protocol_span_attributes() -> dict[str, str]
|
||||
```
|
||||
|
||||
|
||||
Get the negotiated MCP protocol version for the current request.
|
||||
|
||||
Mirrors the `mcp.protocol.version` attribute the SDK's own
|
||||
`OpenTelemetryMiddleware` sets — FastMCP drops that middleware to avoid a
|
||||
duplicate SERVER span, so this restores the attribute on FastMCP's span.
|
||||
|
||||
|
||||
### `record_span_exception` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/telemetry.py#L154" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
record_span_exception(span: Span, e: Exception) -> None
|
||||
```
|
||||
|
||||
|
||||
Record an exception and error status on a span.
|
||||
|
||||
|
||||
### `seam_span` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/telemetry.py#L164" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
seam_span(method: str, server_name: str) -> Generator[Span, None, None]
|
||||
```
|
||||
|
||||
|
||||
Open the per-request SERVER span at the FastMCP middleware seam.
|
||||
|
||||
The span is named after the method and carries the base MCP attributes
|
||||
(`mcp.method.name`, `fastmcp.server.name`, auth/session context) so
|
||||
seam-only methods (`logging/setLevel`, `tasks/*`, `ping`, `initialize`, ...)
|
||||
are fully attributed even though they never reach the high-level path. It is
|
||||
marked with `SEAM_SPAN_MARKER` so a later `server_span` call in the
|
||||
high-level path enriches this span with component attributes instead of
|
||||
opening a second one. Exceptions raised anywhere below the seam — including
|
||||
rejections *before* the high-level path (auth, not-found, middleware vetoes)
|
||||
that would otherwise produce no SERVER span at all — are recorded here.
|
||||
|
||||
In `propagation_only` mode no span is opened at all — this is the one place
|
||||
that has to know the difference, because the seam is where the incoming
|
||||
`_meta` parent context is applied for the whole request.
|
||||
|
||||
|
||||
### `server_span` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/telemetry.py#L224" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
server_span(name: str, method: str, server_name: str, component_type: str, component_key: str, resource_uri: str | None = None, tool_name: str | None = None, prompt_name: str | None = None) -> Generator[Span, None, None]
|
||||
```
|
||||
|
||||
|
||||
Emit or enrich a SERVER span with standard MCP attributes and auth context.
|
||||
|
||||
When the current active span is the request's seam span (opened by
|
||||
`FastMCPServerMiddleware` and marked with `SEAM_SPAN_MARKER`), this sets the
|
||||
component attributes on that span and yields it *without* starting a second
|
||||
span — so failures rejected before this point and the successful high-level
|
||||
call share one richly-attributed SERVER span. Otherwise (non-seam contexts,
|
||||
e.g. in-process `mcp.call_tool()` calls that bypass the dispatcher) it opens a
|
||||
new SERVER span as before.
|
||||
|
||||
Automatically records any exception on the span and sets error status.
|
||||
|
||||
In `propagation_only` mode no span is opened or enriched. The seam has
|
||||
normally already attached the incoming parent context for this request;
|
||||
doing it again here is a no-op, and covers the in-process callers that
|
||||
bypass the dispatcher and so never reach the seam at all.
|
||||
|
||||
|
||||
### `delegate_span` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/telemetry.py#L305" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
delegate_span(name: str, provider_type: str, component_key: str, method: str | None = None) -> Generator[Span, None, None]
|
||||
```
|
||||
|
||||
|
||||
Create an INTERNAL span for provider delegation.
|
||||
|
||||
Used by FastMCPProvider when delegating to mounted servers.
|
||||
Automatically records any exception on the span and sets error status.
|
||||
|
||||
193
docs/python-sdk/fastmcp-server-transforms.mdx
Normal file
193
docs/python-sdk/fastmcp-server-transforms.mdx
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
---
|
||||
title: transforms
|
||||
sidebarTitle: transforms
|
||||
---
|
||||
|
||||
# `fastmcp.server.transforms`
|
||||
|
||||
|
||||
Transform system for component transformations.
|
||||
|
||||
Transforms modify components (tools, resources, prompts). List operations use a pure
|
||||
function pattern where transforms receive sequences and return transformed sequences.
|
||||
Get operations use a middleware pattern with `call_next` to chain lookups.
|
||||
|
||||
Unlike middleware (which operates on requests), transforms are observable by the
|
||||
system for task registration, tag filtering, and component introspection.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.transforms import Namespace
|
||||
|
||||
server = FastMCP("Server")
|
||||
mount = server.mount(other_server)
|
||||
mount.add_transform(Namespace("api")) # Tools become api_toolname
|
||||
```
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `GetToolNext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L36" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Protocol for get_tool call_next functions.
|
||||
|
||||
|
||||
### `GetResourceNext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L44" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Protocol for get_resource call_next functions.
|
||||
|
||||
|
||||
### `GetResourceTemplateNext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L52" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Protocol for get_resource_template call_next functions.
|
||||
|
||||
|
||||
### `GetPromptNext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L60" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Protocol for get_prompt call_next functions.
|
||||
|
||||
|
||||
### `Transform` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L68" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Base class for component transformations.
|
||||
|
||||
List operations use a pure function pattern: transforms receive sequences
|
||||
and return transformed sequences. Get operations use a middleware pattern
|
||||
with `call_next` to chain lookups.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `list_tools` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L95" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
|
||||
```
|
||||
|
||||
List tools with transformation applied.
|
||||
|
||||
**Args:**
|
||||
- `tools`: Sequence of tools to transform.
|
||||
|
||||
**Returns:**
|
||||
- Transformed sequence of tools.
|
||||
|
||||
|
||||
#### `get_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_tool(self, name: str, call_next: GetToolNext) -> Tool | None
|
||||
```
|
||||
|
||||
Get a tool by name.
|
||||
|
||||
**Args:**
|
||||
- `name`: The requested tool name (may be transformed).
|
||||
- `call_next`: Callable to get tool from downstream.
|
||||
- `version`: Optional version filter to apply.
|
||||
|
||||
**Returns:**
|
||||
- The tool if found, None otherwise.
|
||||
|
||||
|
||||
#### `list_resources` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L125" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]
|
||||
```
|
||||
|
||||
List resources with transformation applied.
|
||||
|
||||
**Args:**
|
||||
- `resources`: Sequence of resources to transform.
|
||||
|
||||
**Returns:**
|
||||
- Transformed sequence of resources.
|
||||
|
||||
|
||||
#### `get_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L136" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_resource(self, uri: str, call_next: GetResourceNext) -> Resource | None
|
||||
```
|
||||
|
||||
Get a resource by URI.
|
||||
|
||||
**Args:**
|
||||
- `uri`: The requested resource URI (may be transformed).
|
||||
- `call_next`: Callable to get resource from downstream.
|
||||
- `version`: Optional version filter to apply.
|
||||
|
||||
**Returns:**
|
||||
- The resource if found, None otherwise.
|
||||
|
||||
|
||||
#### `list_resource_templates` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L159" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate]
|
||||
```
|
||||
|
||||
List resource templates with transformation applied.
|
||||
|
||||
**Args:**
|
||||
- `templates`: Sequence of resource templates to transform.
|
||||
|
||||
**Returns:**
|
||||
- Transformed sequence of resource templates.
|
||||
|
||||
|
||||
#### `get_resource_template` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L172" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_resource_template(self, uri: str, call_next: GetResourceTemplateNext) -> ResourceTemplate | None
|
||||
```
|
||||
|
||||
Get a resource template by URI.
|
||||
|
||||
**Args:**
|
||||
- `uri`: The requested template URI (may be transformed).
|
||||
- `call_next`: Callable to get template from downstream.
|
||||
- `version`: Optional version filter to apply.
|
||||
|
||||
**Returns:**
|
||||
- The resource template if found, None otherwise.
|
||||
|
||||
|
||||
#### `list_prompts` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L195" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]
|
||||
```
|
||||
|
||||
List prompts with transformation applied.
|
||||
|
||||
**Args:**
|
||||
- `prompts`: Sequence of prompts to transform.
|
||||
|
||||
**Returns:**
|
||||
- Transformed sequence of prompts.
|
||||
|
||||
|
||||
#### `get_prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/transforms/__init__.py#L206" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_prompt(self, name: str, call_next: GetPromptNext) -> Prompt | None
|
||||
```
|
||||
|
||||
Get a prompt by name.
|
||||
|
||||
**Args:**
|
||||
- `name`: The requested prompt name (may be transformed).
|
||||
- `call_next`: Callable to get prompt from downstream.
|
||||
- `version`: Optional version filter to apply.
|
||||
|
||||
**Returns:**
|
||||
- The prompt if found, None otherwise.
|
||||
|
||||
|
|
@ -7,7 +7,7 @@ sidebarTitle: settings
|
|||
|
||||
## Classes
|
||||
|
||||
### `Settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L32" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `Settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
FastMCP settings.
|
||||
|
|
@ -15,7 +15,7 @@ FastMCP settings.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `get_setting` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L44" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `get_setting` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L46" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_setting(self, attr: str) -> Any
|
||||
|
|
@ -25,7 +25,7 @@ Get a setting. If the setting contains one or more `__`, it will be
|
|||
treated as a nested setting.
|
||||
|
||||
|
||||
#### `set_setting` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L57" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `set_setting` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L59" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_setting(self, attr: str, value: Any) -> None
|
||||
|
|
@ -35,7 +35,7 @@ Set a setting. If the setting contains one or more `__`, it will be
|
|||
treated as a nested setting.
|
||||
|
||||
|
||||
#### `normalize_log_level` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L79" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `normalize_log_level` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/settings.py#L81" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
normalize_log_level(cls, v)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,52 @@ Example usage with SDK:
|
|||
|
||||
## Functions
|
||||
|
||||
### `get_tracer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L81" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `telemetry_mode` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L86" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
telemetry_mode() -> 'TelemetryMode'
|
||||
```
|
||||
|
||||
|
||||
Resolve the effective telemetry mode for the current context.
|
||||
|
||||
This is `fastmcp.settings.telemetry_mode`, except that an active
|
||||
`suppress_fastmcp_telemetry()` block downgrades `native` to
|
||||
`propagation_only`. Suppression never upgrades or overrides `off`: `off`
|
||||
means FastMCP touches nothing, and a narrower request to skip FastMCP's
|
||||
spans cannot re-enable the context propagation `off` deliberately omits.
|
||||
|
||||
|
||||
### `native_spans_enabled` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L103" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
native_spans_enabled() -> bool
|
||||
```
|
||||
|
||||
|
||||
Whether FastMCP should create its own spans right now.
|
||||
|
||||
|
||||
### `suppress_fastmcp_telemetry` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L109" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
suppress_fastmcp_telemetry() -> Iterator[None]
|
||||
```
|
||||
|
||||
|
||||
Suppress FastMCP's own spans without disabling trace propagation.
|
||||
|
||||
Scoped equivalent of `telemetry_mode="propagation_only"`, for callers that
|
||||
embed FastMCP inside their own instrumented stack and want to own the MCP
|
||||
span hierarchy for a specific block. Narrower than OpenTelemetry's global
|
||||
instrumentation suppression: only FastMCP's spans are skipped, so nested
|
||||
instrumentation (HTTP clients, databases) keeps emitting, and trace context
|
||||
still flows through `_meta` so those spans are parented correctly.
|
||||
|
||||
Has no effect when `telemetry_mode` is already `off`.
|
||||
|
||||
|
||||
### `get_tracer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L128" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_tracer(version: str | None = None) -> Tracer
|
||||
|
|
@ -42,21 +87,22 @@ Get the FastMCP tracer for creating spans.
|
|||
|
||||
Instrumentation is on by default. FastMCP uses only the OpenTelemetry API,
|
||||
so span creation is a no-op with negligible overhead unless an OpenTelemetry
|
||||
SDK and exporter are configured. Set `fastmcp.settings.enable_telemetry` to
|
||||
False (env `FASTMCP_ENABLE_TELEMETRY=false`) to turn instrumentation off
|
||||
entirely, in which case this returns a pass-through tracer that leaves the
|
||||
current OTel context untouched even when an SDK is configured.
|
||||
SDK and exporter are configured. When `fastmcp.settings.telemetry_mode` is
|
||||
`propagation_only` or `off` — or the caller is inside a
|
||||
`suppress_fastmcp_telemetry()` block — this returns a pass-through tracer
|
||||
that creates no spans and leaves the current OTel context untouched even
|
||||
when an SDK is configured.
|
||||
|
||||
**Args:**
|
||||
- `version`: Optional version string for the instrumentation
|
||||
|
||||
**Returns:**
|
||||
- A tracer instance. Returns a non-attaching pass-through tracer if
|
||||
- telemetry is disabled; span creation is otherwise a no-op unless an SDK
|
||||
- is configured.
|
||||
- A tracer instance. Returns a non-attaching pass-through tracer when
|
||||
- FastMCP's own spans are disabled; span creation is otherwise a no-op
|
||||
- unless an SDK is configured.
|
||||
|
||||
|
||||
### `inject_trace_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `inject_trace_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L152" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
inject_trace_context(meta: dict[str, Any] | None = None) -> dict[str, Any] | None
|
||||
|
|
@ -73,7 +119,7 @@ Inject current trace context into a meta dict for MCP request propagation.
|
|||
- or None if no trace context to inject and meta was None
|
||||
|
||||
|
||||
### `record_span_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L132" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `record_span_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L183" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
record_span_error(span: Span, exception: BaseException) -> None
|
||||
|
|
@ -83,7 +129,7 @@ record_span_error(span: Span, exception: BaseException) -> None
|
|||
Record an exception on a span and set error status.
|
||||
|
||||
|
||||
### `restore_dropped_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L158" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `restore_dropped_attributes` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L209" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
restore_dropped_attributes(span: Span, attrs: Mapping[str, otel_types.AttributeValue]) -> None
|
||||
|
|
@ -133,7 +179,7 @@ kept at call sites so it reads alongside the sibling `is_recording()`
|
|||
guards already in those functions.
|
||||
|
||||
|
||||
### `extract_trace_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L212" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `extract_trace_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/telemetry.py#L263" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
extract_trace_context(meta: dict[str, Any] | None) -> Context
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ callers.
|
|||
|
||||
## Functions
|
||||
|
||||
### `parse_docstring` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/docstring_parsing.py#L35" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `parse_docstring` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/docstring_parsing.py#L33" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
parse_docstring(fn: Callable[..., Any]) -> ParsedDocstring
|
||||
|
|
@ -32,7 +32,7 @@ docstring as the description with no parameter descriptions.
|
|||
|
||||
## Classes
|
||||
|
||||
### `ParsedDocstring` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/docstring_parsing.py#L28" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ParsedDocstring` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/docstring_parsing.py#L26" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
The extracted description and per-parameter descriptions from a docstring.
|
||||
|
|
|
|||
|
|
@ -7,13 +7,53 @@ sidebarTitle: exceptions
|
|||
|
||||
## Functions
|
||||
|
||||
### `iter_exc` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L36" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `is_http_status_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L19" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
is_http_status_error(exc: BaseException) -> bool
|
||||
```
|
||||
|
||||
|
||||
Return whether an exception is an httpx2 or legacy-httpx status error.
|
||||
|
||||
|
||||
### `get_http_status_code` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L26" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_http_status_code(exc: BaseException) -> int | None
|
||||
```
|
||||
|
||||
|
||||
Return the response status code from a recognized HTTP status error.
|
||||
|
||||
|
||||
### `is_timeout_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
is_timeout_error(exc: BaseException) -> bool
|
||||
```
|
||||
|
||||
|
||||
Return whether an exception is an httpx2 or legacy-httpx timeout.
|
||||
|
||||
|
||||
### `is_request_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L41" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
is_request_error(exc: BaseException) -> bool
|
||||
```
|
||||
|
||||
|
||||
Return whether an exception is an httpx2 or legacy-httpx request error.
|
||||
|
||||
|
||||
### `iter_exc` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L48" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
iter_exc(group: BaseExceptionGroup)
|
||||
```
|
||||
|
||||
### `get_catch_handlers` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L64" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `get_catch_handlers` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/exceptions.py#L76" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_catch_handlers() -> Mapping[type[BaseException] | Iterable[type[BaseException]], Callable[[BaseExceptionGroup[Any]], Any]]
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ Extract information from a FastMCP v1.x instance using a Client.
|
|||
- FastMCPInfo dataclass containing the extracted information
|
||||
|
||||
|
||||
### `inspect_fastmcp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L411" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `inspect_fastmcp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L413" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
inspect_fastmcp(mcp: FastMCP[Any] | SDKServer) -> FastMCPInfo
|
||||
|
|
@ -61,7 +61,7 @@ and uses the appropriate extraction method.
|
|||
- FastMCPInfo dataclass containing the extracted information
|
||||
|
||||
|
||||
### `format_fastmcp_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L436" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `format_fastmcp_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L438" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
format_fastmcp_info(info: FastMCPInfo) -> bytes
|
||||
|
|
@ -73,7 +73,7 @@ Format FastMCPInfo as FastMCP-specific JSON.
|
|||
This includes FastMCP-specific fields like tags, enabled, annotations, etc.
|
||||
|
||||
|
||||
### `format_mcp_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L465" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `format_mcp_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L467" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
format_mcp_info(mcp: FastMCP[Any] | SDKServer) -> bytes
|
||||
|
|
@ -86,7 +86,7 @@ Uses Client to get the standard MCP protocol format with camelCase fields.
|
|||
Includes version metadata at the top level.
|
||||
|
||||
|
||||
### `format_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L500" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `format_info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L502" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
format_info(mcp: FastMCP[Any] | SDKServer, format: InspectFormat | Literal['fastmcp', 'mcp'], info: FastMCPInfo | None = None) -> bytes
|
||||
|
|
@ -136,7 +136,7 @@ Information about a resource template.
|
|||
Information extracted from a FastMCP instance.
|
||||
|
||||
|
||||
### `InspectFormat` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L429" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `InspectFormat` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/inspect.py#L431" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Output format for inspect command.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,17 @@ sidebarTitle: json_schema
|
|||
|
||||
## Functions
|
||||
|
||||
### `require_discriminator_property` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L149" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `replace_refs` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L7" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
replace_refs(*args: Any, **kwargs: Any) -> Any
|
||||
```
|
||||
|
||||
|
||||
Call jsonref lazily while preserving the module's patchable boundary.
|
||||
|
||||
|
||||
### `require_discriminator_property` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L154" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
require_discriminator_property(schema: dict[str, Any]) -> dict[str, Any]
|
||||
|
|
@ -24,7 +34,7 @@ model with ``union_tag_not_found``. No-op if there is no string
|
|||
``propertyName``.
|
||||
|
||||
|
||||
### `dereference_refs` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L180" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `dereference_refs` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L185" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
dereference_refs(schema: dict[str, Any]) -> dict[str, Any]
|
||||
|
|
@ -57,7 +67,7 @@ schemas from untrusted servers.
|
|||
- when no longer needed
|
||||
|
||||
|
||||
### `resolve_root_ref` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L327" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `resolve_root_ref` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L336" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
resolve_root_ref(schema: dict[str, Any]) -> dict[str, Any]
|
||||
|
|
@ -79,7 +89,7 @@ the referenced definition while preserving $defs for nested references.
|
|||
- if no resolution is needed
|
||||
|
||||
|
||||
### `compress_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L741" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `compress_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/json_schema.py#L750" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
compress_schema(schema: dict[str, Any], prune_params: list[str] | None = None, prune_additional_properties: bool = False, prune_titles: bool = False, dereference: bool = False) -> dict[str, Any]
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Logging utilities for FastMCP.
|
|||
|
||||
## Functions
|
||||
|
||||
### `get_logger` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/logging.py#L14" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `get_logger` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/logging.py#L27" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_logger(name: str) -> logging.Logger
|
||||
|
|
@ -26,7 +26,7 @@ Get a logger nested under FastMCP namespace.
|
|||
- a configured logger instance
|
||||
|
||||
|
||||
### `configure_logging` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/logging.py#L29" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `configure_logging` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/logging.py#L42" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
configure_logging(level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] | int = 'INFO', logger: logging.Logger | None = None, enable_rich_tracebacks: bool | None = None, **rich_kwargs: Any) -> None
|
||||
|
|
@ -41,7 +41,7 @@ Configure logging for FastMCP.
|
|||
- `rich_kwargs`: the parameters to use for creating RichHandler
|
||||
|
||||
|
||||
### `temporary_log_level` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/logging.py#L117" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `temporary_log_level` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/logging.py#L127" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
temporary_log_level(level: str | None, logger: logging.Logger | None = None, enable_rich_tracebacks: bool | None = None, **rich_kwargs: Any)
|
||||
|
|
|
|||
61
docs/python-sdk/fastmcp-utilities-prefab.mdx
Normal file
61
docs/python-sdk/fastmcp-utilities-prefab.mdx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
title: prefab
|
||||
sidebarTitle: prefab
|
||||
---
|
||||
|
||||
# `fastmcp.utilities.prefab`
|
||||
|
||||
|
||||
Lazy helpers for FastMCP's optional Prefab UI integration.
|
||||
|
||||
## Functions
|
||||
|
||||
### `prefab_available` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/prefab.py#L12" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
prefab_available() -> bool
|
||||
```
|
||||
|
||||
|
||||
Return whether Prefab UI is installed without importing it.
|
||||
|
||||
|
||||
### `is_prefab_type` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/prefab.py#L42" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
is_prefab_type(candidate: Any) -> bool
|
||||
```
|
||||
|
||||
|
||||
Return whether a type is a Prefab app or component type.
|
||||
|
||||
|
||||
### `is_prefab_app` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/prefab.py#L51" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
is_prefab_app(value: Any) -> bool
|
||||
```
|
||||
|
||||
|
||||
Return whether a value is a Prefab app.
|
||||
|
||||
|
||||
### `is_prefab_component` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/prefab.py#L60" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
is_prefab_component(value: Any) -> bool
|
||||
```
|
||||
|
||||
|
||||
Return whether a value is a Prefab component.
|
||||
|
||||
|
||||
### `prefab_app_from_component` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/utilities/prefab.py#L69" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
prefab_app_from_component(component: Any) -> Any
|
||||
```
|
||||
|
||||
|
||||
Wrap a Prefab component in a Prefab app.
|
||||
|
||||
|
|
@ -315,8 +315,10 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
|
|||
**`"remember"` — silent consent on return:**
|
||||
Users see the consent screen on first authorization; subsequent flows from the same browser for the same `(client_id, redirect_uri)` are silently approved via a signed cookie. Cross-site navigations (detected via `Sec-Fetch-Site`) fall back to the prompt. `Sec-Fetch-Site` is a browser-level heuristic rather than a protocol guarantee: an attacker who finds a way to initiate a non-cross-site navigation (XSS on a sibling origin, a same-site redirect chain, etc.) can reach the silent-consent path. `True` does not depend on this signal. See [Confused Deputy Attacks](#confused-deputy-attacks) for the underlying attack class.
|
||||
|
||||
**`"external"` — delegate to upstream:**
|
||||
Skip the built-in consent page; consent is collected by the upstream IdP or a custom login page referenced via `upstream_authorization_endpoint`. No security warning is logged.
|
||||
**`"external"` — externally managed:**
|
||||
Follows the same authorization path as `False`: FastMCP skips its consent page and associated browser-binding protections, then redirects directly to the upstream provider. The difference is logging. `False` emits a security warning, while `"external"` suppresses that warning as an explicit acknowledgment that the operator is enforcing equivalent consent and transaction-binding protections elsewhere. FastMCP does not provide or verify those external protections.
|
||||
|
||||
Ordinary upstream OAuth consent is generally not equivalent. It typically authorizes FastMCP's shared upstream application without identifying the downstream MCP client or binding approval to that client's transaction. Use `"external"` only when your surrounding authorization system supplies those protections.
|
||||
|
||||
**`False` — disable entirely:**
|
||||
Authorization proceeds directly to the upstream provider without any consent UI. Logs a security warning. Only for local development or testing.
|
||||
|
|
@ -336,7 +338,7 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
|
|||
```
|
||||
|
||||
<Warning>
|
||||
Disabling consent removes an important security layer. Only disable for local development or testing environments where you fully control all connecting clients.
|
||||
Both `False` and `"external"` disable FastMCP's consent and browser-binding protections. `False` warns about this configuration; `"external"` suppresses the warning because it is an operator acknowledgment that equivalent protections exist elsewhere. Prefer the default `True` unless you own that external authorization flow.
|
||||
</Warning>
|
||||
</ParamField>
|
||||
|
||||
|
|
@ -387,6 +389,10 @@ The OAuth proxy requires a compatible `TokenVerifier` to validate tokens from yo
|
|||
|
||||
See the [Token Verification guide](/servers/auth/token-verification) for detailed setup instructions for your provider.
|
||||
|
||||
<Warning>
|
||||
Provider-specific verifiers like `GitHubTokenVerifier` and `GoogleTokenVerifier` confirm that a token is a valid credential for that provider — not that it was issued to *your* application. GitHub tokens carry no audience claim at all, so any valid GitHub credential (including a personal access token) will verify. Inside the OAuth proxy this is safe: the proxy issues its own tokens to clients and only runs the verifier against upstream tokens it obtained through its own OAuth flow. If you use one of these verifiers standalone, you are authenticating "any user of that provider" unless you constrain it — `GoogleTokenVerifier` accepts an `audience` parameter to pin tokens to your OAuth client ID.
|
||||
</Warning>
|
||||
|
||||
### Scope Configuration
|
||||
|
||||
OAuth scopes control what permissions your application requests from users. They're configured through your `TokenVerifier` (required for the OAuth proxy to validate tokens from your provider). Set `required_scopes` to automatically request the permissions your application needs:
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ auth = OIDCProxy(
|
|||
</ParamField>
|
||||
|
||||
<ParamField body="require_authorization_consent" type='bool | Literal["remember", "external"]' default="True">
|
||||
Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (consent handled by upstream IdP or custom page), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs.
|
||||
Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (same authorization path as `False`, but the warning is suppressed because the operator asserts that equivalent protections are enforced externally), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="consent_csp_policy" type="str | None" default="None">
|
||||
|
|
|
|||
|
|
@ -80,6 +80,19 @@ This configuration creates a server that validates JWTs issued by `auth.yourcomp
|
|||
|
||||
The `issuer` parameter ensures tokens come from your trusted authentication system, while `audience` validation prevents tokens intended for other services from being accepted by your MCP server.
|
||||
|
||||
`JWTVerifier` accepts RSA (`RS*` and `PS*`), ECDSA (`ES*`), and Edwards-curve (`Ed25519` and `Ed448`) signatures from JWKS endpoints. Set `algorithm` when your issuer does not use the default `RS256`:
|
||||
|
||||
```python
|
||||
verifier = JWTVerifier(
|
||||
jwks_uri="https://auth.yourcompany.com/.well-known/jwks.json",
|
||||
issuer="https://auth.yourcompany.com",
|
||||
audience="mcp-production-api",
|
||||
algorithm="Ed25519",
|
||||
)
|
||||
```
|
||||
|
||||
The legacy `EdDSA` identifier is also accepted for compatibility with identity providers that have not yet adopted the fully specified identifiers from RFC 9864.
|
||||
|
||||
### Symmetric Key Verification (HMAC)
|
||||
|
||||
Symmetric key verification uses a shared secret for both signing and validation, making it ideal for internal microservices and trusted environments where the same secret can be securely distributed to both token issuers and validators.
|
||||
|
|
@ -121,7 +134,7 @@ The parameter is named `public_key` for backwards compatibility, but when using
|
|||
|
||||
### Static Public Key Verification
|
||||
|
||||
Static public key verification works when you have a fixed RSA or ECDSA signing key and don't need automatic key rotation. This approach is primarily useful for development environments or controlled deployments where JWKS endpoints aren't available.
|
||||
Static public key verification works when you have a fixed RSA, ECDSA, or EdDSA signing key and don't need automatic key rotation. This approach is primarily useful for development environments or controlled deployments where JWKS endpoints aren't available.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
|
@ -141,7 +154,7 @@ verifier = JWTVerifier(
|
|||
mcp = FastMCP(name="Protected API", auth=verifier)
|
||||
```
|
||||
|
||||
This configuration validates tokens using a specific RSA or ECDSA public key. The key must correspond to the private key used by your token issuer. While less flexible than JWKS endpoints, this approach can be useful in development environments or when testing with fixed keys.
|
||||
This configuration validates tokens using a specific RSA, ECDSA, or EdDSA public key. The key must correspond to the private key used by your token issuer. While less flexible than JWKS endpoints, this approach can be useful in development environments or when testing with fixed keys.
|
||||
## Opaque Token Verification
|
||||
|
||||
Many authorization servers issue opaque tokens rather than self-contained JWTs. Opaque tokens are random strings that carry no information themselves - the authorization server maintains their state and validation requires querying the server. FastMCP supports opaque token validation through OAuth 2.0 Token Introspection (RFC 7662).
|
||||
|
|
@ -425,4 +438,3 @@ mcp = FastMCP(name="Production API", auth=verifier)
|
|||
This keeps configuration out of your codebase while maintaining explicit setup.
|
||||
|
||||
This approach enables the same codebase to run across development, staging, and production environments with different authentication requirements. Development might use static tokens while production uses JWT verification, all controlled through environment configuration.
|
||||
|
||||
|
|
|
|||
|
|
@ -430,4 +430,57 @@ async def call_api(endpoint: str, client: dict = Depends(get_api_client)) -> str
|
|||
return f"Calling {client['base_url']}/{client['version']}/{endpoint}"
|
||||
```
|
||||
|
||||
### Call Arguments
|
||||
|
||||
<VersionBadge version="4.0.0" />
|
||||
|
||||
A dependency factory can read the arguments of the function it serves. Declare the reference with `CallArgument()`:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.dependencies import CallArgument, Depends
|
||||
|
||||
mcp = FastMCP("Call Arguments Demo")
|
||||
|
||||
|
||||
def get_account(user_id: str = CallArgument()) -> dict:
|
||||
return {"id": user_id, "plan": "pro"}
|
||||
|
||||
|
||||
@mcp.tool
|
||||
async def show_account(user_id: str, account: dict = Depends(get_account)) -> str:
|
||||
return f"{account['id']} is on {account['plan']}"
|
||||
```
|
||||
|
||||
When a client calls `show_account`, the factory receives the same `user_id` value the tool receives. The bare form takes the name of the parameter it is declared on. `CallArgument("user_id")` names the parameter explicitly. The reference also sees a value that another dependency on the tool's signature produced. `CallArgument("tenant", optional=True)` yields `None` when the function has no such parameter. References that form a cycle raise `CycleError`, importable from `fastmcp.dependencies`.
|
||||
|
||||
Clients still cannot override dependencies this way: an argument whose name collides with a dependency parameter is stripped before resolution, so a `CallArgument` reference to that parameter resolves the dependency itself.
|
||||
|
||||
### Bindings
|
||||
|
||||
<VersionBadge version="4.0.0" />
|
||||
|
||||
`Depends()` accepts keyword bindings, so you can wire up a factory without changing it:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.dependencies import CallArgument, Depends
|
||||
|
||||
mcp = FastMCP("Bindings Demo")
|
||||
|
||||
|
||||
def get_account(user_id: str) -> dict:
|
||||
return {"id": user_id, "plan": "pro"}
|
||||
|
||||
|
||||
@mcp.tool
|
||||
async def show_account(
|
||||
owner: str,
|
||||
account: dict = Depends(get_account, user_id=CallArgument("owner")),
|
||||
) -> str:
|
||||
return f"{account['id']} is on {account['plan']}"
|
||||
```
|
||||
|
||||
A binding that is a `Dependency`, such as `CallArgument(...)` or another `Depends(...)`, resolves first and the factory receives its value. Any other value passes through as it is. A binding replaces the default of the factory's own parameter, which is then never resolved. Two dependencies on the same factory share one cached result only when their bindings match. See the [Docket dependency documentation](https://docket.lol/en/latest/dependency-injection/) for more detail on call arguments and bindings.
|
||||
|
||||
For advanced dependency patterns—like `TaskArgument()` for accessing task parameters, or custom `Dependency` subclasses—see the [Docket dependency documentation](https://chrisguidry.github.io/docket/dependencies/).
|
||||
|
|
|
|||
|
|
@ -310,6 +310,22 @@ async def on_initialize(self, context: MiddlewareContext, call_next):
|
|||
Rejection works only **before** `call_next()`. Raising `McpError` afterward logs the error without sending it — the client still receives a successful initialize response.
|
||||
</Warning>
|
||||
|
||||
#### on_discover
|
||||
|
||||
Called when a modern client negotiates through `server/discover`. Core discovery responses are returned as `DiscoverResult`; extension-owned result types are returned as dictionaries and should be passed through unless the middleware handles that extension.
|
||||
|
||||
```python
|
||||
from mcp_types import DiscoverResult
|
||||
|
||||
async def on_discover(self, context, call_next):
|
||||
result = await call_next(context)
|
||||
if not isinstance(result, DiscoverResult):
|
||||
return result
|
||||
return result.model_copy(update={"instructions": "Custom instructions"})
|
||||
```
|
||||
|
||||
Fields such as `supported_versions`, `capabilities`, and cache policy should only be changed when the server's public behavior also changes.
|
||||
|
||||
### Raw Handler
|
||||
|
||||
For complete control over all messages, override `__call__` instead of individual hooks:
|
||||
|
|
|
|||
|
|
@ -60,11 +60,9 @@ To mount a proxy inside another FastMCP server, see [Mounting External Servers](
|
|||
|
||||
## Connection Semantics
|
||||
|
||||
FastMCP proxies are lazy bridges. Creating the proxy object and starting the local server do not contact the upstream server. The upstream connection begins when an MCP client sends an `initialize` request to the proxy.
|
||||
FastMCP proxies are lazy bridges. Creating the proxy object and starting the local server do not contact the upstream server. During client negotiation, the proxy makes a best-effort request for optional server metadata using the backend client's existing lifecycle and negotiation mode; an unavailable backend does not prevent the client from connecting to the proxy.
|
||||
|
||||
During initialization, the proxy initializes the upstream server before responding locally. If the upstream server is unavailable, the URL does not point to an MCP endpoint, or upstream authentication cannot complete, the proxy initialization fails. This keeps the local proxy's connection status aligned with the upstream server it represents.
|
||||
|
||||
After initialization, the proxy forwards MCP requests such as `ping`, `tools/list`, `resources/list`, `prompts/list`, tool calls, resource reads, sampling, elicitation, logging, and progress through the upstream client.
|
||||
Subsequent MCP requests such as `ping`, `tools/list`, `resources/list`, `prompts/list`, tool calls, resource reads, sampling, elicitation, logging, and progress connect to the backend as needed. Component provider failures follow `provider_error_strategy`: the default `"warn"` logs and skips a failed provider, while `"raise"` reports the failure to the client.
|
||||
|
||||
## Transport Bridging
|
||||
|
||||
|
|
@ -240,6 +238,10 @@ proxy = create_proxy(
|
|||
|
||||
A modern client here reaches both `weather` and `calendar` on modern sessions, so a guard tool on either one round-trips end to end. An explicit `mode` pins every backend in the configuration, the same way it pins a single one.
|
||||
|
||||
### Request Metadata
|
||||
|
||||
Request `_meta` follows the same connection boundary. Progress tokens, tracing, task state, and application or vendor metadata pass through the proxy to the backend. The connection-owned keys — protocol version, client identity, and client capabilities — never copy from the frontend connection: a modern backend session stamps its own negotiated values, and a handshake-era backend receives none. This holds even when the two connections negotiate different eras, such as a modern client reaching a handshake-only backend through an explicit `mode`.
|
||||
|
||||
## Configuration-Based Proxies
|
||||
|
||||
<VersionBadge version="2.4.0" />
|
||||
|
|
@ -384,6 +386,28 @@ Only reuse sessions when you know the backend is stateless (e.g. stateless HTTP)
|
|||
|
||||
## Advanced Usage
|
||||
|
||||
### Forwarding Server Metadata
|
||||
|
||||
Add `ProxyMetadataMiddleware` when a gateway built with `ProxyProvider` should also expose backend instructions and namespaced `_meta`:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.providers.proxy import (
|
||||
ProxyClient,
|
||||
ProxyMetadataMiddleware,
|
||||
ProxyProvider,
|
||||
)
|
||||
|
||||
backend = ProxyProvider(lambda: ProxyClient("http://backend:8000/mcp", mode="auto"))
|
||||
gateway = FastMCP(
|
||||
"Controlled Gateway",
|
||||
providers=[backend],
|
||||
middleware=[ProxyMetadataMiddleware(backend)],
|
||||
)
|
||||
```
|
||||
|
||||
By default the gateway keeps its own `serverInfo`; pass `identity="upstream"` to use the backend identity when available. Frontend instructions and `_meta` values win on collisions. The middleware never copies upstream protocol versions, connection metadata, capabilities, cache policy, `resultType`, or unknown top-level fields. If the backend is unavailable, the client can still connect without its optional metadata.
|
||||
|
||||
### FastMCPProxy Class
|
||||
|
||||
For explicit session control, use `FastMCPProxy` directly:
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ mcp.add_extension(TasksExtension(url="redis://localhost:6379/0", concurrency=20)
|
|||
| `FASTMCP_DOCKET_URL` | `memory://` | Backend URL (`memory://` or `redis://host:port/db`) |
|
||||
| `FASTMCP_DOCKET_NAME` | `fastmcp` | Queue name. Servers and workers sharing a name and URL share a queue. |
|
||||
| `FASTMCP_DOCKET_CONCURRENCY` | `10` | Maximum concurrent tasks per worker. |
|
||||
| `FASTMCP_TASKS_ENCRYPTION_KEY` | (unset) | Encrypts [task context snapshots at rest](#credentials-at-rest). Every server and worker sharing a queue must set the same key. |
|
||||
|
||||
## Backends
|
||||
|
||||
|
|
@ -193,6 +194,28 @@ mcp.add_extension(TasksExtension(url="redis://localhost:6379/0"))
|
|||
- **Fast**: Single-digit millisecond task pickup latency
|
||||
- **Scalable**: Add workers to distribute load across processes or machines
|
||||
|
||||
### Credentials at Rest
|
||||
|
||||
A background task runs long after the request that submitted it has ended, but it still needs to know who asked for the work. FastMCP captures that identity at submission time in a **task context snapshot**: the caller's access token and every inbound HTTP header, including `Authorization`. The worker restores the snapshot before the tool body runs, so `get_access_token()` and `get_http_headers()` return the submitting caller.
|
||||
|
||||
That snapshot lives in the backend for the task's TTL. With `memory://` it never leaves the process. With Redis or Valkey it is a stored value, and by default it is stored as plaintext JSON. A `rediss://` URL encrypts the connection, not the data the backend holds. Anyone who can read the backend can read the tokens.
|
||||
|
||||
Set `FASTMCP_TASKS_ENCRYPTION_KEY` to encrypt the snapshot before it is written:
|
||||
|
||||
```bash
|
||||
export FASTMCP_TASKS_ENCRYPTION_KEY=$(python -c "import secrets; print(secrets.token_urlsafe(32))")
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Every server and worker on the same queue must set the same key. The process that restores a snapshot is rarely the one that captured it, and a worker with the wrong key cannot recover the caller.
|
||||
</Warning>
|
||||
|
||||
With a key configured, restore **fails closed**: a worker that cannot decrypt a snapshot fails the task instead of running the tool with no identity. This matters for a tool whose behavior depends on the caller: running it as an anonymous user is worse than not running it. The failure is reported to the client as a task error, and the server log names the key mismatch.
|
||||
|
||||
Two consequences of failing closed are worth planning for. Tasks submitted before the key was set fail when a worker with the key picks them up, so drain the queue before you roll a key out. Rotating a key does the same to tasks in flight under the old one.
|
||||
|
||||
The key protects the snapshot only. Tool arguments and any answers a task gathers through [mid-task input](#gathering-input-mid-task) are still stored as plaintext, so treat the backend as sensitive regardless.
|
||||
|
||||
## Workers
|
||||
|
||||
Every FastMCP server with task-enabled tools automatically starts an **embedded worker**. You do not need to start a separate worker process for tasks to execute.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,44 @@ icon: "sparkles"
|
|||
tag: NEW
|
||||
---
|
||||
|
||||
<Update label="FastMCP 4.0.0b3" description="August 14, 2026" tags={["Releases"]}>
|
||||
<Card
|
||||
title="FastMCP v4.0.0b3: Fast Fourward"
|
||||
href="https://github.com/PrefectHQ/fastmcp/releases/tag/v4.0.0b3"
|
||||
cta="Read the release notes"
|
||||
>
|
||||
FastMCP 4 beta 3 moves the v4 line toward general availability with new authentication and dependency-injection capabilities, plus compatibility hardening across OAuth, proxies, OpenAPI, and Python 3.14.
|
||||
|
||||
🔐 **Authentication foundations** — Prefect Horizon gains a native authentication client and local state, Google token verification can pin audiences, and Scalekit issuer updates preserve backward compatibility.
|
||||
|
||||
🧰 **Tool dependencies** — `CallArgument` and `Depends` bindings from `uncalled-for` 0.4 work in regular tools and background tasks.
|
||||
|
||||
🔄 **Runtime reliability** — stateful proxy clients reconnect after session failures, consent transactions keep valid earlier CSRF tokens, and partial parameter hints work on Python 3.14.
|
||||
|
||||
🧾 **OpenAPI fidelity** — parameter-level `example` and `examples` values now flow into generated tool schemas.
|
||||
</Card>
|
||||
</Update>
|
||||
|
||||
<Update label="FastMCP 3.4.7" description="August 10, 2026" tags={["Releases"]}>
|
||||
<Card
|
||||
title="FastMCP v3.4.7: Know Your Audience"
|
||||
href="https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.7"
|
||||
cta="Read the release notes"
|
||||
>
|
||||
FastMCP 3.4.7 restores CIMD `private_key_jwt` authentication for bare-origin OAuth proxy deployments by validating client assertions against the exact token endpoint advertised to clients.
|
||||
</Card>
|
||||
</Update>
|
||||
|
||||
<Update label="FastMCP 3.4.6" description="August 5, 2026" tags={["Releases"]}>
|
||||
<Card
|
||||
title="FastMCP v3.4.6: Trust, but Proxy"
|
||||
href="https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.6"
|
||||
cta="Read the release notes"
|
||||
>
|
||||
FastMCP 3.4.6 adds trusted-proxy support for SSRF-protected OAuth metadata and JWKS fetches on the 3.x line. Deployments can route these requests through a mandated corporate proxy while preserving custom CA certificates, and FastMCP refuses the fetch when no proxy is configured instead of risking an unprotected direct request.
|
||||
</Card>
|
||||
</Update>
|
||||
|
||||
<Update label="FastMCP 4.0.0b1" description="July 28, 2026" tags={["Releases"]}>
|
||||
<Card
|
||||
title="FastMCP v4.0.0b1: Fourgone Conclusion"
|
||||
|
|
|
|||
|
|
@ -296,8 +296,10 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
|
|||
**`"remember"` — silent consent on return:**
|
||||
Users see the consent screen on first authorization; subsequent flows from the same browser for the same `(client_id, redirect_uri)` are silently approved via a signed cookie. Cross-site navigations (detected via `Sec-Fetch-Site`) fall back to the prompt. `Sec-Fetch-Site` is a browser-level heuristic rather than a protocol guarantee: an attacker who finds a way to initiate a non-cross-site navigation (XSS on a sibling origin, a same-site redirect chain, etc.) can reach the silent-consent path. `True` does not depend on this signal. See [Confused Deputy Attacks](#confused-deputy-attacks) for the underlying attack class.
|
||||
|
||||
**`"external"` — delegate to upstream:**
|
||||
Skip the built-in consent page; consent is collected by the upstream IdP or a custom login page referenced via `upstream_authorization_endpoint`. No security warning is logged.
|
||||
**`"external"` — externally managed:**
|
||||
Follows the same authorization path as `False`: FastMCP skips its consent page and associated browser-binding protections, then redirects directly to the upstream provider. The difference is logging. `False` emits a security warning, while `"external"` suppresses that warning as an explicit acknowledgment that the operator is enforcing equivalent consent and transaction-binding protections elsewhere. FastMCP does not provide or verify those external protections.
|
||||
|
||||
Ordinary upstream OAuth consent is generally not equivalent. It typically authorizes FastMCP's shared upstream application without identifying the downstream MCP client or binding approval to that client's transaction. Use `"external"` only when your surrounding authorization system supplies those protections.
|
||||
|
||||
**`False` — disable entirely:**
|
||||
Authorization proceeds directly to the upstream provider without any consent UI. Logs a security warning. Only for local development or testing.
|
||||
|
|
@ -317,7 +319,7 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
|
|||
```
|
||||
|
||||
<Warning>
|
||||
Disabling consent removes an important security layer. Only disable for local development or testing environments where you fully control all connecting clients.
|
||||
Both `False` and `"external"` disable FastMCP's consent and browser-binding protections. `False` warns about this configuration; `"external"` suppresses the warning because it is an operator acknowledgment that equivalent protections exist elsewhere. Prefer the default `True` unless you own that external authorization flow.
|
||||
</Warning>
|
||||
</ParamField>
|
||||
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ auth = OIDCProxy(
|
|||
</ParamField>
|
||||
|
||||
<ParamField body="require_authorization_consent" type='bool | Literal["remember", "external"]' default="True">
|
||||
Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (consent handled by upstream IdP or custom page), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/v2/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs.
|
||||
Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (same authorization path as `False`, but the warning is suppressed because the operator asserts that equivalent protections are enforced externally), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/v2/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="consent_csp_policy" type="str | None" default="None">
|
||||
|
|
|
|||
|
|
@ -310,8 +310,10 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
|
|||
**`"remember"` — silent consent on return:**
|
||||
Users see the consent screen on first authorization; subsequent flows from the same browser for the same `(client_id, redirect_uri)` are silently approved via a signed cookie. Cross-site navigations (detected via `Sec-Fetch-Site`) fall back to the prompt. `Sec-Fetch-Site` is a browser-level heuristic rather than a protocol guarantee: an attacker who finds a way to initiate a non-cross-site navigation (XSS on a sibling origin, a same-site redirect chain, etc.) can reach the silent-consent path. `True` does not depend on this signal. See [Confused Deputy Attacks](#confused-deputy-attacks) for the underlying attack class.
|
||||
|
||||
**`"external"` — delegate to upstream:**
|
||||
Skip the built-in consent page; consent is collected by the upstream IdP or a custom login page referenced via `upstream_authorization_endpoint`. No security warning is logged.
|
||||
**`"external"` — externally managed:**
|
||||
Follows the same authorization path as `False`: FastMCP skips its consent page and associated browser-binding protections, then redirects directly to the upstream provider. The difference is logging. `False` emits a security warning, while `"external"` suppresses that warning as an explicit acknowledgment that the operator is enforcing equivalent consent and transaction-binding protections elsewhere. FastMCP does not provide or verify those external protections.
|
||||
|
||||
Ordinary upstream OAuth consent is generally not equivalent. It typically authorizes FastMCP's shared upstream application without identifying the downstream MCP client or binding approval to that client's transaction. Use `"external"` only when your surrounding authorization system supplies those protections.
|
||||
|
||||
**`False` — disable entirely:**
|
||||
Authorization proceeds directly to the upstream provider without any consent UI. Logs a security warning. Only for local development or testing.
|
||||
|
|
@ -331,7 +333,7 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
|
|||
```
|
||||
|
||||
<Warning>
|
||||
Disabling consent removes an important security layer. Only disable for local development or testing environments where you fully control all connecting clients.
|
||||
Both `False` and `"external"` disable FastMCP's consent and browser-binding protections. `False` warns about this configuration; `"external"` suppresses the warning because it is an operator acknowledgment that equivalent protections exist elsewhere. Prefer the default `True` unless you own that external authorization flow.
|
||||
</Warning>
|
||||
</ParamField>
|
||||
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ auth = OIDCProxy(
|
|||
</ParamField>
|
||||
|
||||
<ParamField body="require_authorization_consent" type='bool | Literal["remember", "external"]' default="True">
|
||||
Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (consent handled by upstream IdP or custom page), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs.
|
||||
Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (same authorization path as `False`, but the warning is suppressed because the operator asserts that equivalent protections are enforced externally), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="consent_csp_policy" type="str | None" default="None">
|
||||
|
|
|
|||
|
|
@ -6,15 +6,13 @@ from importlib.metadata import PackageNotFoundError, version as _version
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from fastmcp import _install_hints
|
||||
from fastmcp._warnings import FastMCPDeprecationWarning
|
||||
from fastmcp.settings import Settings
|
||||
from fastmcp.utilities.logging import configure_logging as _configure_logging
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastmcp.client import Client as Client
|
||||
from fastmcp.apps.app import FastMCPApp as FastMCPApp
|
||||
from fastmcp.exceptions import (
|
||||
FastMCPDeprecationWarning as FastMCPDeprecationWarning,
|
||||
)
|
||||
from fastmcp.server.context import Context as Context
|
||||
from fastmcp.server.server import FastMCP as FastMCP
|
||||
|
||||
|
|
@ -39,12 +37,7 @@ except PackageNotFoundError:
|
|||
__version__ = _version("fastmcp")
|
||||
|
||||
if settings.deprecation_warnings:
|
||||
try:
|
||||
from fastmcp.exceptions import FastMCPDeprecationWarning
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
warnings.simplefilter("default", FastMCPDeprecationWarning)
|
||||
warnings.simplefilter("default", FastMCPDeprecationWarning)
|
||||
|
||||
|
||||
# --- Lazy imports for performance (see #3292) ---
|
||||
|
|
@ -81,10 +74,6 @@ def __getattr__(name: str) -> object:
|
|||
raise ImportError(_install_hints.APP_SUPPORT) from exc
|
||||
|
||||
return FastMCPApp
|
||||
if name == "FastMCPDeprecationWarning":
|
||||
from fastmcp.exceptions import FastMCPDeprecationWarning
|
||||
|
||||
return FastMCPDeprecationWarning
|
||||
if name == "client":
|
||||
try:
|
||||
return importlib.import_module("fastmcp.client")
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import warnings
|
|||
|
||||
import mcp_types
|
||||
|
||||
from fastmcp.exceptions import FastMCPDeprecationWarning
|
||||
from fastmcp._warnings import FastMCPDeprecationWarning
|
||||
|
||||
# Map each SDK model class to the camelCase -> snake_case field reads we bridge.
|
||||
# Limited to fields FastMCP users actually read (docs boundary inventory).
|
||||
|
|
|
|||
10
fastmcp_slim/fastmcp/_warnings.py
Normal file
10
fastmcp_slim/fastmcp/_warnings.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
"""Warning types that can be imported without loading FastMCP's exception stack."""
|
||||
|
||||
|
||||
class FastMCPDeprecationWarning(DeprecationWarning):
|
||||
"""Deprecation warning for FastMCP APIs.
|
||||
|
||||
Subclass of DeprecationWarning so that standard warning filters
|
||||
still apply, but FastMCP can selectively enable its own warnings
|
||||
without affecting other libraries in the process.
|
||||
"""
|
||||
1
fastmcp_slim/fastmcp/cli/deploy/__init__.py
Normal file
1
fastmcp_slim/fastmcp/cli/deploy/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Horizon deployment support for the FastMCP CLI."""
|
||||
101
fastmcp_slim/fastmcp/cli/deploy/authentication.py
Normal file
101
fastmcp_slim/fastmcp/cli/deploy/authentication.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
"""Horizon device authorization workflow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import webbrowser
|
||||
from collections.abc import Awaitable, Callable
|
||||
from contextlib import suppress
|
||||
|
||||
from pydantic import SecretStr
|
||||
|
||||
from fastmcp.cli.deploy.horizon_client import (
|
||||
DeviceAuthorization,
|
||||
DeviceMetadata,
|
||||
HorizonClient,
|
||||
)
|
||||
|
||||
|
||||
class DeviceAuthorizationError(RuntimeError):
|
||||
"""Device authorization did not complete."""
|
||||
|
||||
|
||||
class DeviceAuthorizationDeniedError(DeviceAuthorizationError):
|
||||
"""The user denied the device authorization request."""
|
||||
|
||||
|
||||
class DeviceAuthorizationExpiredError(DeviceAuthorizationError):
|
||||
"""The device authorization request expired."""
|
||||
|
||||
|
||||
async def poll_device_authorization(
|
||||
client: HorizonClient,
|
||||
authorization: DeviceAuthorization,
|
||||
*,
|
||||
sleep: Callable[[float], Awaitable[None]] | None = None,
|
||||
monotonic: Callable[[], float] = time.monotonic,
|
||||
) -> SecretStr:
|
||||
"""Poll at the server interval until the device request completes."""
|
||||
sleep = asyncio.sleep if sleep is None else sleep
|
||||
deadline = monotonic() + authorization.expires_in
|
||||
interval = float(authorization.interval)
|
||||
|
||||
while True:
|
||||
remaining = deadline - monotonic()
|
||||
if remaining <= 0:
|
||||
raise DeviceAuthorizationExpiredError(
|
||||
"The device authorization request expired"
|
||||
)
|
||||
|
||||
await sleep(min(interval, remaining))
|
||||
if monotonic() >= deadline:
|
||||
raise DeviceAuthorizationExpiredError(
|
||||
"The device authorization request expired"
|
||||
)
|
||||
|
||||
result = await client.exchange_device_authorization(authorization.device_code)
|
||||
if result.access_token is not None:
|
||||
return result.access_token
|
||||
if result.error == "authorization_pending":
|
||||
continue
|
||||
if result.error == "slow_down":
|
||||
interval += 5
|
||||
continue
|
||||
if result.error == "access_denied":
|
||||
raise DeviceAuthorizationDeniedError(
|
||||
"The device authorization request was denied"
|
||||
)
|
||||
if result.error == "expired_token":
|
||||
raise DeviceAuthorizationExpiredError(
|
||||
"The device authorization request expired"
|
||||
)
|
||||
|
||||
raise DeviceAuthorizationError("Device authorization failed")
|
||||
|
||||
|
||||
async def authorize_device(
|
||||
client: HorizonClient,
|
||||
*,
|
||||
metadata: DeviceMetadata | None = None,
|
||||
on_challenge: Callable[[DeviceAuthorization], None] | None = None,
|
||||
open_browser: bool = False,
|
||||
browser_opener: Callable[[str], object] = webbrowser.open,
|
||||
sleep: Callable[[float], Awaitable[None]] | None = None,
|
||||
monotonic: Callable[[], float] = time.monotonic,
|
||||
) -> SecretStr:
|
||||
"""Create, present, and complete a Horizon device authorization."""
|
||||
authorization = await client.create_device_authorization(metadata)
|
||||
if on_challenge is not None:
|
||||
on_challenge(authorization)
|
||||
|
||||
if open_browser:
|
||||
with suppress(OSError, webbrowser.Error):
|
||||
browser_opener(authorization.verification_uri_complete)
|
||||
|
||||
return await poll_device_authorization(
|
||||
client,
|
||||
authorization,
|
||||
sleep=sleep,
|
||||
monotonic=monotonic,
|
||||
)
|
||||
68
fastmcp_slim/fastmcp/cli/deploy/configuration.py
Normal file
68
fastmcp_slim/fastmcp/cli/deploy/configuration.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
"""Global non-secret configuration for the FastMCP CLI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from fastmcp.cli.deploy.credentials import CredentialStore
|
||||
from fastmcp.cli.deploy.horizon_client import (
|
||||
DEFAULT_HORIZON_API_ORIGIN,
|
||||
normalize_api_origin,
|
||||
)
|
||||
from fastmcp.cli.deploy.state import read_state, state_lock, write_state
|
||||
|
||||
|
||||
class HorizonConfiguration(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True)
|
||||
|
||||
schema_version: Literal[1] = Field(alias="schemaVersion")
|
||||
api_origin: str = Field(alias="apiOrigin")
|
||||
|
||||
@field_validator("api_origin")
|
||||
@classmethod
|
||||
def validate_api_origin(cls, value: str) -> str:
|
||||
return normalize_api_origin(value)
|
||||
|
||||
|
||||
class ConfigurationStore:
|
||||
"""Persist the Horizon API origin without organization state."""
|
||||
|
||||
def __init__(self, state_directory: Path | None = None) -> None:
|
||||
if state_directory is None:
|
||||
import fastmcp
|
||||
|
||||
state_directory = fastmcp.settings.home / "cli"
|
||||
self.path = state_directory / "config.json"
|
||||
|
||||
def load(self) -> HorizonConfiguration:
|
||||
state = read_state(self.path, HorizonConfiguration)
|
||||
if state is not None:
|
||||
return state
|
||||
return HorizonConfiguration(
|
||||
schemaVersion=1,
|
||||
apiOrigin=DEFAULT_HORIZON_API_ORIGIN,
|
||||
)
|
||||
|
||||
def save(self, configuration: HorizonConfiguration) -> None:
|
||||
write_state(
|
||||
self.path,
|
||||
configuration.model_dump(mode="json", by_alias=True),
|
||||
)
|
||||
|
||||
def set_api_origin(
|
||||
self,
|
||||
api_origin: str,
|
||||
*,
|
||||
credentials: CredentialStore,
|
||||
) -> HorizonConfiguration:
|
||||
"""Set the origin and clear credentials before an origin change."""
|
||||
with state_lock(self.path.parent):
|
||||
current = self.load()
|
||||
updated = HorizonConfiguration(schemaVersion=1, apiOrigin=api_origin)
|
||||
if updated.api_origin != current.api_origin:
|
||||
credentials.clear()
|
||||
self.save(updated)
|
||||
return updated
|
||||
142
fastmcp_slim/fastmcp/cli/deploy/credentials.py
Normal file
142
fastmcp_slim/fastmcp/cli/deploy/credentials.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
"""Restricted Horizon credential storage and resolution."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import Awaitable, Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
SecretStr,
|
||||
ValidationError,
|
||||
field_validator,
|
||||
)
|
||||
|
||||
from fastmcp.cli.deploy.horizon_client import HorizonClient, normalize_api_origin
|
||||
from fastmcp.cli.deploy.state import (
|
||||
StateFileError,
|
||||
read_state,
|
||||
remove_state,
|
||||
state_lock,
|
||||
write_state,
|
||||
)
|
||||
|
||||
CredentialSource = Literal["environment", "stored", "interactive"]
|
||||
|
||||
|
||||
class AuthenticationRequiredError(RuntimeError):
|
||||
"""No Horizon credential is available without interactive authorization."""
|
||||
|
||||
|
||||
class AuthState(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True)
|
||||
|
||||
schema_version: Literal[1] = Field(alias="schemaVersion")
|
||||
api_key: SecretStr = Field(alias="apiKey")
|
||||
|
||||
@field_validator("api_key")
|
||||
@classmethod
|
||||
def require_nonempty_api_key(cls, value: SecretStr) -> SecretStr:
|
||||
if not value.get_secret_value().strip():
|
||||
raise ValueError("The API key is empty")
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResolvedCredential:
|
||||
api_key: SecretStr
|
||||
source: CredentialSource
|
||||
|
||||
|
||||
class CredentialStore:
|
||||
"""Persist the active personal Horizon API key."""
|
||||
|
||||
def __init__(self, state_directory: Path | None = None) -> None:
|
||||
if state_directory is None:
|
||||
import fastmcp
|
||||
|
||||
state_directory = fastmcp.settings.home / "cli"
|
||||
self.path = state_directory / "auth.json"
|
||||
|
||||
def load(self) -> SecretStr | None:
|
||||
state = read_state(self.path, AuthState, secret=True)
|
||||
return state.api_key if state is not None else None
|
||||
|
||||
def save(self, api_key: SecretStr | str) -> None:
|
||||
try:
|
||||
state = AuthState(schemaVersion=1, apiKey=api_key)
|
||||
except ValidationError:
|
||||
raise StateFileError("The Horizon API key is invalid") from None
|
||||
write_state(
|
||||
self.path,
|
||||
{
|
||||
"schemaVersion": state.schema_version,
|
||||
"apiKey": state.api_key.get_secret_value(),
|
||||
},
|
||||
)
|
||||
|
||||
def save_for_origin(
|
||||
self,
|
||||
api_key: SecretStr | str,
|
||||
*,
|
||||
expected_api_origin: str,
|
||||
) -> None:
|
||||
"""Save a key only while its issuing Horizon origin is active."""
|
||||
from fastmcp.cli.deploy.configuration import ConfigurationStore
|
||||
|
||||
expected_api_origin = normalize_api_origin(expected_api_origin)
|
||||
with state_lock(self.path.parent):
|
||||
active_api_origin = ConfigurationStore(self.path.parent).load().api_origin
|
||||
if active_api_origin != expected_api_origin:
|
||||
raise StateFileError("The Horizon host changed during login")
|
||||
self.save(api_key)
|
||||
|
||||
def clear(self) -> None:
|
||||
remove_state(self.path)
|
||||
|
||||
|
||||
async def resolve_credential(
|
||||
store: CredentialStore,
|
||||
*,
|
||||
environ: Mapping[str, str] | None = None,
|
||||
authorize: Callable[[], Awaitable[SecretStr]] | None = None,
|
||||
expected_api_origin: str | None = None,
|
||||
) -> ResolvedCredential:
|
||||
"""Resolve environment, stored, then interactive credentials."""
|
||||
environ = os.environ if environ is None else environ
|
||||
environment_key = environ.get("HORIZON_API_KEY")
|
||||
if environment_key:
|
||||
return ResolvedCredential(
|
||||
api_key=SecretStr(environment_key),
|
||||
source="environment",
|
||||
)
|
||||
|
||||
stored_key = store.load()
|
||||
if stored_key is not None:
|
||||
return ResolvedCredential(api_key=stored_key, source="stored")
|
||||
|
||||
if authorize is None:
|
||||
raise AuthenticationRequiredError("Horizon authentication is required")
|
||||
|
||||
api_key = await authorize()
|
||||
if expected_api_origin is None:
|
||||
store.save(api_key)
|
||||
else:
|
||||
store.save_for_origin(api_key, expected_api_origin=expected_api_origin)
|
||||
return ResolvedCredential(api_key=api_key, source="interactive")
|
||||
|
||||
|
||||
async def revoke_and_clear_credential(
|
||||
client: HorizonClient,
|
||||
store: CredentialStore,
|
||||
) -> None:
|
||||
"""Attempt remote revocation and always remove the stored credential."""
|
||||
try:
|
||||
await client.revoke_current_api_key()
|
||||
finally:
|
||||
store.clear()
|
||||
332
fastmcp_slim/fastmcp/cli/deploy/horizon_client.py
Normal file
332
fastmcp_slim/fastmcp/cli/deploy/horizon_client.py
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
"""Typed HTTP client for the Horizon control plane."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from types import TracebackType
|
||||
from typing import Annotated, Literal, TypeVar
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
import httpx2
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
SecretStr,
|
||||
ValidationError,
|
||||
field_validator,
|
||||
)
|
||||
|
||||
DEVICE_AUTH_CLIENT_ID = "fastmcp-cli"
|
||||
DEVICE_AUTH_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"
|
||||
DEFAULT_HORIZON_API_ORIGIN = "https://horizon.prefect.io"
|
||||
|
||||
DeviceTokenError = Literal[
|
||||
"authorization_pending",
|
||||
"slow_down",
|
||||
"access_denied",
|
||||
"expired_token",
|
||||
]
|
||||
|
||||
|
||||
class HorizonError(RuntimeError):
|
||||
"""A safe Horizon client error."""
|
||||
|
||||
|
||||
class HorizonUnavailableError(HorizonError):
|
||||
"""The Horizon API could not be reached."""
|
||||
|
||||
|
||||
class HorizonUnauthorizedError(HorizonError):
|
||||
"""The Horizon credential was rejected."""
|
||||
|
||||
|
||||
class HorizonResponseError(HorizonError):
|
||||
"""Horizon returned an unexpected response."""
|
||||
|
||||
def __init__(self, message: str, *, status_code: int | None = None) -> None:
|
||||
super().__init__(message)
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
class _ResponseModel(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", frozen=True)
|
||||
|
||||
|
||||
ResponseModelT = TypeVar("ResponseModelT", bound=_ResponseModel)
|
||||
|
||||
|
||||
class DeviceAuthorization(_ResponseModel):
|
||||
device_code: Annotated[str, Field(min_length=1)]
|
||||
user_code: Annotated[str, Field(min_length=1)]
|
||||
verification_uri: Annotated[str, Field(pattern=r"^https?://")]
|
||||
verification_uri_complete: Annotated[str, Field(pattern=r"^https?://")]
|
||||
expires_in: Annotated[int, Field(gt=0)]
|
||||
interval: Annotated[int, Field(gt=0)]
|
||||
|
||||
|
||||
class DeviceAccessToken(_ResponseModel):
|
||||
access_token: SecretStr
|
||||
token_type: Literal["Bearer"]
|
||||
|
||||
@field_validator("access_token")
|
||||
@classmethod
|
||||
def require_nonempty_access_token(cls, value: SecretStr) -> SecretStr:
|
||||
if not value.get_secret_value().strip():
|
||||
raise ValueError("The access token is empty")
|
||||
return value
|
||||
|
||||
|
||||
class _DeviceTokenErrorResponse(_ResponseModel):
|
||||
error: DeviceTokenError
|
||||
|
||||
|
||||
class HorizonUser(_ResponseModel):
|
||||
id: str
|
||||
email: str
|
||||
name: str | None
|
||||
|
||||
|
||||
class _CurrentUserResponse(_ResponseModel):
|
||||
user: HorizonUser
|
||||
|
||||
|
||||
class HorizonOrganization(_ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
slug: str
|
||||
|
||||
|
||||
class _PaginationMeta(_ResponseModel):
|
||||
nextCursor: str | None
|
||||
limit: int
|
||||
|
||||
|
||||
class _OrganizationsResponse(_ResponseModel):
|
||||
items: tuple[HorizonOrganization, ...]
|
||||
meta: _PaginationMeta
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DeviceMetadata:
|
||||
device_name: str | None = None
|
||||
platform: str | None = None
|
||||
architecture: str | None = None
|
||||
client_version: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DeviceTokenPoll:
|
||||
access_token: SecretStr | None = None
|
||||
error: DeviceTokenError | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if (self.access_token is None) == (self.error is None):
|
||||
raise ValueError("A device token poll must contain one result")
|
||||
|
||||
|
||||
def normalize_api_origin(value: str) -> str:
|
||||
"""Validate and normalize a Horizon API origin."""
|
||||
parts = urlsplit(value)
|
||||
try:
|
||||
_ = parts.port
|
||||
except ValueError:
|
||||
raise ValueError("The Horizon API origin must be an HTTP origin") from None
|
||||
if (
|
||||
parts.scheme not in {"http", "https"}
|
||||
or not parts.hostname
|
||||
or parts.username is not None
|
||||
or parts.password is not None
|
||||
or parts.query
|
||||
or parts.fragment
|
||||
or parts.path not in {"", "/"}
|
||||
):
|
||||
raise ValueError("The Horizon API origin must be an HTTP origin")
|
||||
|
||||
return urlunsplit((parts.scheme, parts.netloc, "", "", ""))
|
||||
|
||||
|
||||
class HorizonClient:
|
||||
"""Call the Horizon routes used by FastMCP CLI authentication."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_origin: str = DEFAULT_HORIZON_API_ORIGIN,
|
||||
*,
|
||||
api_key: SecretStr | str | None = None,
|
||||
transport: httpx2.AsyncBaseTransport | None = None,
|
||||
timeout: float = 30.0,
|
||||
) -> None:
|
||||
self.api_origin = normalize_api_origin(api_origin)
|
||||
self._api_key = (
|
||||
api_key
|
||||
if isinstance(api_key, SecretStr)
|
||||
else SecretStr(api_key)
|
||||
if api_key is not None
|
||||
else None
|
||||
)
|
||||
self._client = httpx2.AsyncClient(
|
||||
base_url=self.api_origin,
|
||||
follow_redirects=False,
|
||||
timeout=timeout,
|
||||
transport=transport,
|
||||
)
|
||||
|
||||
async def __aenter__(self) -> HorizonClient:
|
||||
await self._client.__aenter__()
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
await self._client.__aexit__(exc_type, exc_value, traceback)
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await self._client.aclose()
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
authenticated: bool = False,
|
||||
data: Mapping[str, str] | None = None,
|
||||
params: Mapping[str, str | int] | None = None,
|
||||
) -> httpx2.Response:
|
||||
headers: dict[str, str] = {}
|
||||
if authenticated:
|
||||
if self._api_key is None:
|
||||
raise HorizonUnauthorizedError("Horizon authentication is required")
|
||||
headers["Authorization"] = f"Bearer {self._api_key.get_secret_value()}"
|
||||
|
||||
try:
|
||||
response = await self._client.request(
|
||||
method,
|
||||
path,
|
||||
headers=headers,
|
||||
data=data,
|
||||
params=params,
|
||||
)
|
||||
except httpx2.RequestError as exc:
|
||||
raise HorizonUnavailableError("The Horizon API is unavailable") from exc
|
||||
|
||||
if authenticated and response.status_code == 401:
|
||||
raise HorizonUnauthorizedError("The Horizon credential is not valid")
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _validate_response(
|
||||
response: httpx2.Response,
|
||||
model: type[ResponseModelT],
|
||||
) -> ResponseModelT:
|
||||
try:
|
||||
return model.model_validate_json(response.content)
|
||||
except (ValidationError, ValueError):
|
||||
raise HorizonResponseError(
|
||||
"Horizon returned an invalid response",
|
||||
status_code=response.status_code,
|
||||
) from None
|
||||
|
||||
@staticmethod
|
||||
def _require_status(response: httpx2.Response, expected: int) -> None:
|
||||
if response.status_code != expected:
|
||||
raise HorizonResponseError(
|
||||
"Horizon returned an unexpected status",
|
||||
status_code=response.status_code,
|
||||
)
|
||||
|
||||
async def create_device_authorization(
|
||||
self,
|
||||
metadata: DeviceMetadata | None = None,
|
||||
) -> DeviceAuthorization:
|
||||
metadata = metadata or DeviceMetadata()
|
||||
form = {
|
||||
"client_id": DEVICE_AUTH_CLIENT_ID,
|
||||
"device_name": metadata.device_name,
|
||||
"platform": metadata.platform,
|
||||
"architecture": metadata.architecture,
|
||||
"client_version": metadata.client_version,
|
||||
}
|
||||
response = await self._request(
|
||||
"POST",
|
||||
"/api/v0/oauth/device/authorization",
|
||||
data={key: value for key, value in form.items() if value is not None},
|
||||
)
|
||||
self._require_status(response, 200)
|
||||
return self._validate_response(response, DeviceAuthorization)
|
||||
|
||||
async def exchange_device_authorization(
|
||||
self,
|
||||
device_code: str,
|
||||
) -> DeviceTokenPoll:
|
||||
response = await self._request(
|
||||
"POST",
|
||||
"/api/v0/oauth/device/token",
|
||||
data={
|
||||
"grant_type": DEVICE_AUTH_GRANT_TYPE,
|
||||
"client_id": DEVICE_AUTH_CLIENT_ID,
|
||||
"device_code": device_code,
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = self._validate_response(response, DeviceAccessToken)
|
||||
return DeviceTokenPoll(access_token=result.access_token)
|
||||
|
||||
if response.status_code == 400:
|
||||
result = self._validate_response(response, _DeviceTokenErrorResponse)
|
||||
return DeviceTokenPoll(error=result.error)
|
||||
|
||||
self._require_status(response, 200)
|
||||
raise AssertionError("unreachable")
|
||||
|
||||
async def get_current_user(self) -> HorizonUser:
|
||||
response = await self._request(
|
||||
"GET",
|
||||
"/api/v0/me",
|
||||
authenticated=True,
|
||||
)
|
||||
self._require_status(response, 200)
|
||||
result = self._validate_response(response, _CurrentUserResponse)
|
||||
return result.user
|
||||
|
||||
async def list_organizations(self) -> tuple[HorizonOrganization, ...]:
|
||||
organizations: list[HorizonOrganization] = []
|
||||
cursor: str | None = None
|
||||
seen_cursors: set[str] = set()
|
||||
|
||||
while True:
|
||||
params = {"limit": 100}
|
||||
if cursor is not None:
|
||||
params["cursor"] = cursor
|
||||
response = await self._request(
|
||||
"GET",
|
||||
"/api/v0/me/organizations",
|
||||
authenticated=True,
|
||||
params=params,
|
||||
)
|
||||
self._require_status(response, 200)
|
||||
result = self._validate_response(response, _OrganizationsResponse)
|
||||
organizations.extend(result.items)
|
||||
|
||||
cursor = result.meta.nextCursor
|
||||
if cursor is None:
|
||||
return tuple(organizations)
|
||||
if cursor in seen_cursors:
|
||||
raise HorizonResponseError(
|
||||
"Horizon returned an invalid organization cursor",
|
||||
status_code=response.status_code,
|
||||
)
|
||||
seen_cursors.add(cursor)
|
||||
|
||||
async def revoke_current_api_key(self) -> None:
|
||||
response = await self._request(
|
||||
"DELETE",
|
||||
"/api/v0/me/api-key",
|
||||
authenticated=True,
|
||||
)
|
||||
self._require_status(response, 204)
|
||||
226
fastmcp_slim/fastmcp/cli/deploy/state.py
Normal file
226
fastmcp_slim/fastmcp/cli/deploy/state.py
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
"""Versioned JSON state helpers for the FastMCP CLI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager, suppress
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
ModelT = TypeVar("ModelT", bound=BaseModel)
|
||||
|
||||
|
||||
class StateFileError(RuntimeError):
|
||||
"""A CLI state file could not be read or written safely."""
|
||||
|
||||
|
||||
_WINDOWS_ACL_SCRIPT = r"""
|
||||
$ErrorActionPreference = "Stop"
|
||||
$path = $env:FASTMCP_STATE_PATH
|
||||
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
||||
$acl = Get-Acl -LiteralPath $path
|
||||
$acl.SetAccessRuleProtection($true, $false)
|
||||
foreach ($existingRule in @($acl.Access)) {
|
||||
$acl.RemoveAccessRuleSpecific($existingRule)
|
||||
}
|
||||
|
||||
if ([System.IO.Directory]::Exists($path)) {
|
||||
$inheritance = [System.Security.AccessControl.InheritanceFlags]::ContainerInherit `
|
||||
-bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
|
||||
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
|
||||
$sid,
|
||||
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
||||
$inheritance,
|
||||
[System.Security.AccessControl.PropagationFlags]::None,
|
||||
[System.Security.AccessControl.AccessControlType]::Allow
|
||||
)
|
||||
} else {
|
||||
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
|
||||
$sid,
|
||||
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
||||
[System.Security.AccessControl.AccessControlType]::Allow
|
||||
)
|
||||
}
|
||||
|
||||
$acl.AddAccessRule($rule)
|
||||
Set-Acl -LiteralPath $path -AclObject $acl
|
||||
"""
|
||||
|
||||
|
||||
def _restrict_windows_access(path: Path) -> None:
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"powershell.exe",
|
||||
"-NoLogo",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
_WINDOWS_ACL_SCRIPT,
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env={**os.environ, "FASTMCP_STATE_PATH": str(path)},
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError) as exc:
|
||||
raise StateFileError("Could not restrict access to CLI state") from exc
|
||||
|
||||
|
||||
def _restrict_access(path: Path, *, directory: bool = False) -> None:
|
||||
try:
|
||||
if os.name == "nt":
|
||||
_restrict_windows_access(path)
|
||||
else:
|
||||
path.chmod(0o700 if directory else 0o600)
|
||||
except OSError as exc:
|
||||
raise StateFileError("Could not restrict access to CLI state") from exc
|
||||
|
||||
|
||||
def _prepare_directory(path: Path) -> None:
|
||||
try:
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
except OSError as exc:
|
||||
raise StateFileError("Could not create the CLI state directory") from exc
|
||||
_restrict_access(path, directory=True)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def state_lock(directory: Path) -> Iterator[None]:
|
||||
"""Lock related CLI state changes across processes."""
|
||||
_prepare_directory(directory)
|
||||
lock_path = directory / ".state.lock"
|
||||
if lock_path.is_symlink():
|
||||
raise StateFileError("The CLI state lock must not be a symbolic link")
|
||||
|
||||
lock_file = None
|
||||
try:
|
||||
lock_file = lock_path.open("a+b")
|
||||
_restrict_access(lock_path)
|
||||
if os.name == "nt":
|
||||
import msvcrt
|
||||
|
||||
if lock_path.stat().st_size == 0:
|
||||
lock_file.write(b"\0")
|
||||
lock_file.flush()
|
||||
lock_file.seek(0)
|
||||
msvcrt.locking(lock_file.fileno(), msvcrt.LK_LOCK, 1)
|
||||
else:
|
||||
import fcntl
|
||||
|
||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
|
||||
except (OSError, StateFileError) as exc:
|
||||
if lock_file is not None:
|
||||
with suppress(OSError):
|
||||
lock_file.close()
|
||||
if isinstance(exc, StateFileError):
|
||||
raise
|
||||
raise StateFileError("Could not lock CLI state") from exc
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if os.name == "nt":
|
||||
import msvcrt
|
||||
|
||||
with suppress(OSError):
|
||||
lock_file.seek(0)
|
||||
msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
|
||||
else:
|
||||
import fcntl
|
||||
|
||||
with suppress(OSError):
|
||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
|
||||
with suppress(OSError):
|
||||
lock_file.close()
|
||||
|
||||
|
||||
def read_state(
|
||||
path: Path,
|
||||
model: type[ModelT],
|
||||
*,
|
||||
secret: bool = False,
|
||||
) -> ModelT | None:
|
||||
"""Read and validate a versioned JSON state file."""
|
||||
if not path.exists():
|
||||
return None
|
||||
if path.is_symlink():
|
||||
raise StateFileError(f"CLI state must not be a symbolic link: {path.name}")
|
||||
|
||||
if secret:
|
||||
_restrict_access(path.parent, directory=True)
|
||||
_restrict_access(path)
|
||||
|
||||
try:
|
||||
return model.model_validate_json(path.read_text(encoding="utf-8"))
|
||||
except (ValidationError, ValueError):
|
||||
raise StateFileError(f"CLI state is invalid: {path.name}") from None
|
||||
except OSError as exc:
|
||||
raise StateFileError(f"Could not read CLI state: {path.name}") from exc
|
||||
|
||||
|
||||
def write_state(path: Path, data: dict[str, Any]) -> None:
|
||||
"""Write JSON through a restricted temporary file and atomic replacement."""
|
||||
_prepare_directory(path.parent)
|
||||
payload = (json.dumps(data, indent=2, sort_keys=True) + "\n").encode()
|
||||
descriptor: int | None = None
|
||||
temporary_path: Path | None = None
|
||||
|
||||
try:
|
||||
descriptor, temporary_name = tempfile.mkstemp(
|
||||
dir=path.parent,
|
||||
prefix=f".{path.name}.",
|
||||
suffix=".tmp",
|
||||
)
|
||||
temporary_path = Path(temporary_name)
|
||||
if os.name != "nt":
|
||||
os.fchmod(descriptor, 0o600)
|
||||
|
||||
temporary_file = os.fdopen(descriptor, "wb")
|
||||
descriptor = None
|
||||
with temporary_file:
|
||||
temporary_file.write(payload)
|
||||
temporary_file.flush()
|
||||
os.fsync(temporary_file.fileno())
|
||||
|
||||
_restrict_access(temporary_path)
|
||||
os.replace(temporary_path, path)
|
||||
temporary_path = None
|
||||
|
||||
if os.name != "nt":
|
||||
directory_descriptor = os.open(path.parent, os.O_RDONLY)
|
||||
try:
|
||||
try:
|
||||
os.fsync(directory_descriptor)
|
||||
except OSError as exc:
|
||||
unsupported = {errno.EINVAL, errno.ENOTSUP}
|
||||
if exc.errno not in unsupported:
|
||||
raise
|
||||
finally:
|
||||
os.close(directory_descriptor)
|
||||
except StateFileError:
|
||||
raise
|
||||
except OSError as exc:
|
||||
raise StateFileError(f"Could not write CLI state: {path.name}") from exc
|
||||
finally:
|
||||
if descriptor is not None:
|
||||
with suppress(OSError):
|
||||
os.close(descriptor)
|
||||
if temporary_path is not None:
|
||||
with suppress(OSError):
|
||||
temporary_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def remove_state(path: Path) -> None:
|
||||
"""Remove a state file when it exists."""
|
||||
try:
|
||||
path.unlink(missing_ok=True)
|
||||
except OSError as exc:
|
||||
raise StateFileError(f"Could not remove CLI state: {path.name}") from exc
|
||||
|
|
@ -97,24 +97,33 @@ def _parse_mcp_servers(
|
|||
if not servers_dict:
|
||||
return []
|
||||
|
||||
normalized = {
|
||||
name: _normalize_server_entry(entry)
|
||||
for name, entry in servers_dict.items()
|
||||
if isinstance(entry, dict)
|
||||
}
|
||||
discovered: list[DiscoveredServer] = []
|
||||
for name, entry in servers_dict.items():
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
|
||||
try:
|
||||
config = MCPConfig.from_dict({"mcpServers": normalized})
|
||||
except Exception as exc:
|
||||
logger.warning("Could not parse MCP servers from %s: %s", config_path, exc)
|
||||
return []
|
||||
normalized = _normalize_server_entry(entry)
|
||||
try:
|
||||
config = MCPConfig.from_dict({"mcpServers": {name: normalized}})
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Could not parse MCP server %r from %s: %s",
|
||||
name,
|
||||
config_path,
|
||||
exc,
|
||||
)
|
||||
continue
|
||||
|
||||
return [
|
||||
DiscoveredServer(
|
||||
name=name, source=source, config=server, config_path=config_path
|
||||
discovered.append(
|
||||
DiscoveredServer(
|
||||
name=name,
|
||||
source=source,
|
||||
config=config.mcpServers[name],
|
||||
config_path=config_path,
|
||||
)
|
||||
)
|
||||
for name, server in config.mcpServers.items()
|
||||
]
|
||||
|
||||
return discovered
|
||||
|
||||
|
||||
def _parse_mcp_config(path: Path, source: str) -> list[DiscoveredServer]:
|
||||
|
|
|
|||
|
|
@ -657,6 +657,11 @@ class Client(
|
|||
|
||||
return self._session_state.session
|
||||
|
||||
@property
|
||||
def prior_discover(self) -> mcp_types.DiscoverResult | None:
|
||||
"""The configured result to adopt when `mode` pins a modern version."""
|
||||
return self._prior_discover
|
||||
|
||||
@property
|
||||
def initialize_result(self) -> mcp_types.InitializeResult | None:
|
||||
"""Get the result of the initialization request.
|
||||
|
|
@ -1022,7 +1027,10 @@ class Client(
|
|||
raise RuntimeError(
|
||||
"Session task completed without exception but connection failed"
|
||||
)
|
||||
raise _connection_failure(exception) from exception
|
||||
failure = _connection_failure(exception)
|
||||
if failure is exception:
|
||||
raise exception
|
||||
raise failure from exception
|
||||
|
||||
self._session_state.nesting_counter += 1
|
||||
|
||||
|
|
|
|||
|
|
@ -35,8 +35,7 @@ def create_roots_callback(
|
|||
handler: RootsList | RootsHandler,
|
||||
) -> ListRootsFnT:
|
||||
if isinstance(handler, list):
|
||||
# TODO(ty): remove when ty supports isinstance union narrowing
|
||||
return _create_roots_callback_from_roots(handler) # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
|
||||
return _create_roots_callback_from_roots(handler)
|
||||
elif callable(handler):
|
||||
return _create_roots_callback_from_fn(handler)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ using the uncalled-for DI engine. The docket-specific dependencies
|
|||
|
||||
from typing import Any
|
||||
|
||||
from uncalled_for import Dependency, Depends, Shared
|
||||
from uncalled_for import CallArgument, CycleError, Dependency, Depends, Shared
|
||||
|
||||
from fastmcp.server.dependencies import (
|
||||
CurrentAccessToken,
|
||||
|
|
@ -25,11 +25,13 @@ from fastmcp.server.dependencies import (
|
|||
)
|
||||
|
||||
__all__ = [
|
||||
"CallArgument",
|
||||
"CurrentAccessToken",
|
||||
"CurrentContext",
|
||||
"CurrentFastMCP",
|
||||
"CurrentHeaders",
|
||||
"CurrentRequest",
|
||||
"CycleError",
|
||||
"Dependency",
|
||||
"Depends",
|
||||
"Progress",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ from typing import Any
|
|||
|
||||
from mcp_types import INTERNAL_ERROR, INVALID_PARAMS, ErrorData
|
||||
|
||||
from fastmcp import _warnings
|
||||
|
||||
try:
|
||||
from mcp import MCPError
|
||||
except ImportError:
|
||||
|
|
@ -30,14 +32,7 @@ except ImportError:
|
|||
# see the migration notes.
|
||||
McpError = MCPError
|
||||
|
||||
|
||||
class FastMCPDeprecationWarning(DeprecationWarning):
|
||||
"""Deprecation warning for FastMCP APIs.
|
||||
|
||||
Subclass of DeprecationWarning so that standard warning filters
|
||||
still apply, but FastMCP can selectively enable its own warnings
|
||||
without affecting other libraries in the process.
|
||||
"""
|
||||
FastMCPDeprecationWarning = _warnings.FastMCPDeprecationWarning
|
||||
|
||||
|
||||
class FastMCPError(Exception):
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ from fastmcp import _install_hints
|
|||
if TYPE_CHECKING:
|
||||
from fastmcp.client.transports import (
|
||||
ClientTransport,
|
||||
FastMCPTransport,
|
||||
SSETransport,
|
||||
StdioTransport,
|
||||
StreamableHttpTransport,
|
||||
|
|
@ -153,7 +154,7 @@ class _TransformingMCPServerMixin(BaseModel):
|
|||
|
||||
return wrapped_mcp_server, transport
|
||||
|
||||
def to_transport(self) -> ClientTransport:
|
||||
def to_transport(self) -> FastMCPTransport:
|
||||
"""Get the transport for the transforming MCP server."""
|
||||
try:
|
||||
from fastmcp.client.transports import FastMCPTransport
|
||||
|
|
@ -209,7 +210,7 @@ class StdioMCPServer(BaseModel):
|
|||
|
||||
model_config = ConfigDict(extra="allow") # Preserve unknown fields
|
||||
|
||||
def to_transport(self) -> StdioTransport:
|
||||
def to_transport(self) -> StdioTransport | FastMCPTransport:
|
||||
from fastmcp.client.transports import StdioTransport
|
||||
|
||||
return StdioTransport(
|
||||
|
|
@ -261,7 +262,9 @@ class RemoteMCPServer(BaseModel):
|
|||
extra="allow", arbitrary_types_allowed=True
|
||||
) # Preserve unknown fields
|
||||
|
||||
def to_transport(self) -> StreamableHttpTransport | SSETransport:
|
||||
def to_transport(
|
||||
self,
|
||||
) -> StreamableHttpTransport | SSETransport | FastMCPTransport:
|
||||
from fastmcp.client.transports import (
|
||||
SSETransport,
|
||||
StreamableHttpTransport,
|
||||
|
|
|
|||
|
|
@ -217,7 +217,10 @@ class FunctionPrompt(Prompt):
|
|||
schema_str = json.dumps(param_schema, separators=(",", ":"))
|
||||
|
||||
# Append schema info to description
|
||||
schema_note = f"Provide as a JSON string matching the following schema: {schema_str}"
|
||||
schema_note = (
|
||||
"Provide a value matching the following JSON schema: "
|
||||
f"{schema_str}. Encode non-string values as JSON."
|
||||
)
|
||||
if arg_description:
|
||||
arg_description = f"{arg_description}\n\n{schema_note}"
|
||||
else:
|
||||
|
|
@ -263,26 +266,38 @@ class FunctionPrompt(Prompt):
|
|||
if param_name in sig.parameters:
|
||||
param = sig.parameters[param_name]
|
||||
|
||||
# If parameter has no annotation or annotation is str, pass as-is
|
||||
if (
|
||||
param.annotation == inspect.Parameter.empty
|
||||
or param.annotation is str
|
||||
) or not isinstance(param_value, str):
|
||||
if param.annotation == inspect.Parameter.empty or not isinstance(
|
||||
param_value, str
|
||||
):
|
||||
converted_kwargs[param_name] = param_value
|
||||
else:
|
||||
# Try to convert string argument using type adapter
|
||||
try:
|
||||
adapter = get_cached_typeadapter(param.annotation)
|
||||
# Try JSON parsing first for complex types
|
||||
# Preserve the MCP wire string when validation keeps it
|
||||
# as a string. Non-string results still prefer JSON
|
||||
# decoding so coercible types such as bytes and Path do
|
||||
# not retain JSON quote characters.
|
||||
try:
|
||||
python_value = adapter.validate_python(param_value)
|
||||
except (ValueError, TypeError, pydantic_core.ValidationError):
|
||||
converted_kwargs[param_name] = adapter.validate_json(
|
||||
param_value
|
||||
)
|
||||
except (ValueError, TypeError, pydantic_core.ValidationError):
|
||||
# Fallback to direct validation
|
||||
converted_kwargs[param_name] = adapter.validate_python(
|
||||
param_value
|
||||
)
|
||||
else:
|
||||
if isinstance(python_value, str):
|
||||
converted_kwargs[param_name] = python_value
|
||||
else:
|
||||
try:
|
||||
converted_kwargs[param_name] = (
|
||||
adapter.validate_json(param_value)
|
||||
)
|
||||
except (
|
||||
ValueError,
|
||||
TypeError,
|
||||
pydantic_core.ValidationError,
|
||||
):
|
||||
converted_kwargs[param_name] = python_value
|
||||
except (ValueError, TypeError, pydantic_core.ValidationError) as e:
|
||||
# If conversion fails, provide informative error
|
||||
raise PromptError(
|
||||
|
|
|
|||
|
|
@ -1,17 +1,31 @@
|
|||
import importlib
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from fastmcp import _install_hints
|
||||
|
||||
try:
|
||||
from .context import Context
|
||||
from .server import FastMCP, create_proxy
|
||||
except ImportError as exc:
|
||||
raise ImportError(_install_hints.SERVER_SUPPORT) from exc
|
||||
if TYPE_CHECKING:
|
||||
from .context import Context as Context
|
||||
from .server import FastMCP as FastMCP
|
||||
from .server import create_proxy as create_proxy
|
||||
|
||||
|
||||
def __getattr__(name: str) -> object:
|
||||
if name == "dependencies":
|
||||
return importlib.import_module("fastmcp.server.dependencies")
|
||||
if name in {"context", "dependencies"}:
|
||||
return importlib.import_module(f"fastmcp.server.{name}")
|
||||
if name == "Context":
|
||||
try:
|
||||
from .context import Context
|
||||
except ImportError as exc:
|
||||
raise ImportError(_install_hints.SERVER_SUPPORT) from exc
|
||||
|
||||
return Context
|
||||
if name in {"FastMCP", "create_proxy"}:
|
||||
try:
|
||||
from .server import FastMCP, create_proxy
|
||||
except ImportError as exc:
|
||||
raise ImportError(_install_hints.SERVER_SUPPORT) from exc
|
||||
|
||||
return FastMCP if name == "FastMCP" else create_proxy
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,11 @@ from pydantic import AnyUrl
|
|||
from starlette.requests import Request
|
||||
from starlette.responses import HTMLResponse, RedirectResponse
|
||||
|
||||
from fastmcp.server.auth.oauth_proxy.models import ProxyDCRClient
|
||||
from fastmcp.server.auth.oauth_proxy.models import (
|
||||
ConsentCSRFToken,
|
||||
ProxyDCRClient,
|
||||
_hash_token,
|
||||
)
|
||||
from fastmcp.server.auth.oauth_proxy.ui import create_consent_html
|
||||
from fastmcp.server.auth.redirect_validation import (
|
||||
build_client_redirect,
|
||||
|
|
@ -36,9 +40,39 @@ if TYPE_CHECKING:
|
|||
# Keeps the Cookie header bounded to avoid hitting reverse proxy header limits.
|
||||
_MAX_REMEMBERED_CLIENTS = 25
|
||||
|
||||
# Maximum number of consent-state cookies the browser carries at once. Each
|
||||
# render of a consent page adds one, and a handful of renders is normal (a
|
||||
# reload, a preload, an extension re-fetching the URL); the bound keeps the
|
||||
# Cookie header from growing without limit across many pending flows.
|
||||
#
|
||||
# The matching server-side state is bounded by its own 15-minute TTL rather
|
||||
# than by a count, because counting entries would mean reading them back and
|
||||
# rewriting them — the read-modify-write that concurrent renders race on.
|
||||
_MAX_CSRF_TOKENS = 10
|
||||
|
||||
# Base name of the consent-state cookie. One cookie is set per issued CSRF
|
||||
# token (`MCP_CONSENT_STATE_<digest>`) rather than one list shared by all of
|
||||
# them: two renders in flight at once both build their Set-Cookie from the same
|
||||
# inbound Cookie header, so a shared list silently drops whichever entry was
|
||||
# written first. Separate names never collide.
|
||||
#
|
||||
# The unsuffixed name is the pre-upgrade flat list. It is read, never written,
|
||||
# so a consent page rendered before an upgrade can still be submitted after it.
|
||||
_CONSENT_STATE_COOKIE_BASE = "MCP_CONSENT_STATE"
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def _consent_state_base_name(csrf_token: str) -> str:
|
||||
"""Base cookie name carrying a single issued CSRF token.
|
||||
|
||||
The token is hashed rather than used directly so the raw token does not end
|
||||
up in a cookie name, which is far more likely to be logged than its value.
|
||||
"""
|
||||
digest = hashlib.sha256(csrf_token.encode()).hexdigest()[:32]
|
||||
return f"{_CONSENT_STATE_COOKIE_BASE}_{digest}"
|
||||
|
||||
|
||||
class ConsentMixin:
|
||||
"""Mixin class providing consent management functionality for OAuthProxy.
|
||||
|
||||
|
|
@ -179,6 +213,115 @@ class ConsentMixin:
|
|||
path="/",
|
||||
)
|
||||
|
||||
def _read_consent_state_cookies(
|
||||
self: OAuthProxy, request: Request
|
||||
) -> dict[str, tuple[str, float]]:
|
||||
"""Per-token consent-state cookies the browser sent, by cookie name.
|
||||
|
||||
Returns {cookie_name: (txn_id, issued_at)} for every cookie whose
|
||||
signature verifies. Unsigned, tampered, or unparsable cookies are
|
||||
skipped rather than raising, the same way the other cookie readers here
|
||||
treat them.
|
||||
"""
|
||||
prefix = self._cookie_name(f"{_CONSENT_STATE_COOKIE_BASE}_")
|
||||
found: dict[str, tuple[str, float]] = {}
|
||||
for name, raw in request.cookies.items():
|
||||
if not name.startswith(prefix):
|
||||
continue
|
||||
payload = self._verify_cookie(raw)
|
||||
if not payload:
|
||||
logger.debug("Cookie signature verification failed for %s", name)
|
||||
continue
|
||||
try:
|
||||
data = json.loads(base64.b64decode(payload.encode()).decode())
|
||||
except Exception:
|
||||
logger.debug("Failed to decode cookie %s; ignoring", name)
|
||||
continue
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
txn_id = data.get("txn")
|
||||
issued_at = data.get("iat")
|
||||
if isinstance(txn_id, str) and isinstance(issued_at, int | float):
|
||||
found[name] = (txn_id, float(issued_at))
|
||||
return found
|
||||
|
||||
def _set_consent_state_cookie(
|
||||
self: OAuthProxy,
|
||||
response: HTMLResponse | RedirectResponse,
|
||||
csrf_token: str,
|
||||
txn_id: str,
|
||||
issued_at: float,
|
||||
) -> None:
|
||||
"""Record that this browser received `csrf_token`, under its own name.
|
||||
|
||||
The cookie is what makes the double-submit check meaningful: the token
|
||||
in the form has to match one this browser was actually handed. Writing
|
||||
it under a name derived from the token keeps that property while making
|
||||
the write independent of every other render's.
|
||||
"""
|
||||
payload = base64.b64encode(
|
||||
json.dumps(
|
||||
{"txn": txn_id, "iat": issued_at}, separators=(",", ":")
|
||||
).encode()
|
||||
).decode()
|
||||
response.set_cookie(
|
||||
self._cookie_name(_consent_state_base_name(csrf_token)),
|
||||
self._sign_cookie(payload),
|
||||
max_age=15 * 60,
|
||||
secure=self._is_https,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
path="/",
|
||||
)
|
||||
|
||||
def _clear_consent_state_for_transaction(
|
||||
self: OAuthProxy,
|
||||
request: Request,
|
||||
response: HTMLResponse | RedirectResponse,
|
||||
txn_id: str,
|
||||
*,
|
||||
include_legacy: bool,
|
||||
) -> None:
|
||||
"""Expire the consent state belonging to one completed transaction.
|
||||
|
||||
Only this transaction's cookies are removed. Another consent flow the
|
||||
same browser has open keeps its own state, which a single shared list
|
||||
had no way to express — completing either flow wiped both.
|
||||
"""
|
||||
for name, (cookie_txn, _issued_at) in self._read_consent_state_cookies(
|
||||
request
|
||||
).items():
|
||||
if hmac.compare_digest(cookie_txn, txn_id):
|
||||
self._expire_cookie(response, name)
|
||||
|
||||
if include_legacy:
|
||||
# The pre-upgrade cookie is a flat list with no transaction
|
||||
# attached, so it can only be cleared wholesale. Reached only when
|
||||
# the submitted token came from it, which means every flow sharing
|
||||
# it was rendered before the upgrade too.
|
||||
self._set_list_cookie(
|
||||
response,
|
||||
_CONSENT_STATE_COOKIE_BASE,
|
||||
self._encode_list_cookie([]),
|
||||
max_age=60,
|
||||
)
|
||||
|
||||
def _expire_cookie(
|
||||
self: OAuthProxy,
|
||||
response: HTMLResponse | RedirectResponse,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Expire one cookie by name, matching the attributes it was set with."""
|
||||
response.set_cookie(
|
||||
name,
|
||||
"",
|
||||
max_age=0,
|
||||
secure=self._is_https,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
path="/",
|
||||
)
|
||||
|
||||
def _read_consent_bindings(self: OAuthProxy, request: Request) -> dict[str, str]:
|
||||
"""Read the consent binding map from the signed cookie.
|
||||
|
||||
|
|
@ -369,20 +512,31 @@ class ConsentMixin:
|
|||
sec_fetch_site,
|
||||
)
|
||||
|
||||
# Need consent: issue CSRF token and show HTML
|
||||
# Need consent: issue CSRF token and show HTML.
|
||||
#
|
||||
# A transaction can be rendered more than once before it is submitted —
|
||||
# a reload, a browser preload, an extension re-fetching the URL. Every
|
||||
# render issues its own token, so that a token stays unique to the
|
||||
# browser that received it and the double-submit cookie check keeps its
|
||||
# meaning, and every token issued for the transaction stays valid until
|
||||
# it expires. Dropping the earlier one kills the form the user is
|
||||
# already looking at: they click Approve and get "Invalid or expired
|
||||
# consent token" on a flow that never expired, with no way to recover.
|
||||
#
|
||||
# The token is stored under its own key rather than appended to a list
|
||||
# on the transaction. Two renders in flight at once would both read the
|
||||
# same transaction, each append their token, and the second write would
|
||||
# drop the first — `AsyncKeyValue` has no compare-and-swap to prevent
|
||||
# it. Independent keys make the writes commute, including across
|
||||
# processes sharing one storage backend.
|
||||
csrf_token = secrets.token_urlsafe(32)
|
||||
csrf_expires_at = time.time() + 15 * 60
|
||||
|
||||
# Update transaction with CSRF token
|
||||
txn_model.csrf_token = csrf_token
|
||||
txn_model.csrf_expires_at = csrf_expires_at
|
||||
await self._transaction_store.put(
|
||||
key=txn_id, value=txn_model, ttl=15 * 60
|
||||
) # Auto-expire after 15 minutes
|
||||
|
||||
# Update dict for use in HTML generation
|
||||
txn["csrf_token"] = csrf_token
|
||||
txn["csrf_expires_at"] = csrf_expires_at
|
||||
issued_at = time.time()
|
||||
csrf_expires_at = issued_at + 15 * 60
|
||||
await self._consent_csrf_store.put(
|
||||
key=_hash_token(csrf_token),
|
||||
value=ConsentCSRFToken(txn_id=txn_id, expires_at=csrf_expires_at),
|
||||
ttl=15 * 60, # Auto-expire after 15 minutes
|
||||
)
|
||||
|
||||
# Load client to get client_name and CIMD info if available
|
||||
client = await self.get_client(txn["client_id"])
|
||||
|
|
@ -423,15 +577,19 @@ class ConsentMixin:
|
|||
cimd_domain=cimd_domain,
|
||||
)
|
||||
response = create_secure_html_response(html)
|
||||
# Merge new CSRF token with any existing ones (supports concurrent flows)
|
||||
existing_tokens = self._decode_list_cookie(request, "MCP_CONSENT_STATE")
|
||||
existing_tokens.append(csrf_token)
|
||||
self._set_list_cookie(
|
||||
response,
|
||||
"MCP_CONSENT_STATE",
|
||||
self._encode_list_cookie(existing_tokens),
|
||||
max_age=15 * 60,
|
||||
)
|
||||
self._set_consent_state_cookie(response, csrf_token, txn_id, issued_at)
|
||||
|
||||
# Keep the browser's consent state bounded. The cookie just set always
|
||||
# survives; the oldest of the rest are expired to make room. Eviction
|
||||
# is by issued-at from the cookie itself, so it does not depend on any
|
||||
# server-side bookkeeping that renders would have to share.
|
||||
others = self._read_consent_state_cookies(request)
|
||||
others.pop(self._cookie_name(_consent_state_base_name(csrf_token)), None)
|
||||
surplus = len(others) + 1 - _MAX_CSRF_TOKENS
|
||||
if surplus > 0:
|
||||
by_age = sorted(others.items(), key=lambda item: item[1][1])
|
||||
for name, _entry in by_age[:surplus]:
|
||||
self._expire_cookie(response, name)
|
||||
return response
|
||||
|
||||
async def _submit_consent(
|
||||
|
|
@ -455,10 +613,34 @@ class ConsentMixin:
|
|||
)
|
||||
|
||||
txn = txn_model.model_dump()
|
||||
expected_csrf = txn.get("csrf_token")
|
||||
expires_at = float(txn.get("csrf_expires_at") or 0)
|
||||
|
||||
if not expected_csrf or csrf_token != expected_csrf or time.time() > expires_at:
|
||||
# Look the token up by its own key. A record proves the token was
|
||||
# issued by a render of THIS transaction; nothing else can have written
|
||||
# it, and a concurrent render cannot have removed it.
|
||||
csrf_record = (
|
||||
await self._consent_csrf_store.get(key=_hash_token(csrf_token))
|
||||
if csrf_token
|
||||
else None
|
||||
)
|
||||
if csrf_record is not None:
|
||||
legacy_csrf = False
|
||||
csrf_valid = (
|
||||
hmac.compare_digest(csrf_record.txn_id, txn_id)
|
||||
and time.time() <= csrf_record.expires_at
|
||||
)
|
||||
else:
|
||||
# No record: either the token is bogus, or the consent page was
|
||||
# rendered by a version that kept the token on the transaction.
|
||||
# Honouring the old location keeps a flow that was already open
|
||||
# during an upgrade submittable instead of failing at Approve.
|
||||
stored = txn_model.csrf_token
|
||||
csrf_valid = bool(csrf_token and stored) and (
|
||||
hmac.compare_digest(stored or "", csrf_token)
|
||||
and time.time() <= (txn_model.csrf_expires_at or 0)
|
||||
)
|
||||
legacy_csrf = csrf_valid
|
||||
|
||||
if not csrf_valid:
|
||||
return create_secure_html_response(
|
||||
"<h1>Error</h1><p>Invalid or expired consent token</p>", status_code=400
|
||||
)
|
||||
|
|
@ -467,8 +649,16 @@ class ConsentMixin:
|
|||
# Without this, an attacker who knows their own tx_id/csrf_token can
|
||||
# CSRF the victim's browser into approving consent, bypassing the
|
||||
# consent binding cookie protection.
|
||||
cookie_csrf_tokens = self._decode_list_cookie(request, "MCP_CONSENT_STATE")
|
||||
if csrf_token not in cookie_csrf_tokens:
|
||||
if legacy_csrf:
|
||||
cookie_ok = csrf_token in self._decode_list_cookie(
|
||||
request, _CONSENT_STATE_COOKIE_BASE
|
||||
)
|
||||
else:
|
||||
entry = self._read_consent_state_cookies(request).get(
|
||||
self._cookie_name(_consent_state_base_name(csrf_token))
|
||||
)
|
||||
cookie_ok = entry is not None and hmac.compare_digest(entry[0], txn_id)
|
||||
if not cookie_ok:
|
||||
logger.warning(
|
||||
"CSRF double-submit check failed for transaction %s "
|
||||
"(possible cross-site consent forgery)",
|
||||
|
|
@ -509,9 +699,12 @@ class ConsentMixin:
|
|||
max_age=365 * 24 * 3600,
|
||||
)
|
||||
|
||||
# Clear CSRF cookie by setting empty short-lived value
|
||||
self._set_list_cookie(
|
||||
response, "MCP_CONSENT_STATE", self._encode_list_cookie([]), max_age=60
|
||||
# Retire this transaction's consent state, both halves of it: the
|
||||
# stored token so it cannot be replayed, and the cookies that
|
||||
# carried it. Other pending flows are left alone.
|
||||
await self._consent_csrf_store.delete(key=_hash_token(csrf_token))
|
||||
self._clear_consent_state_for_transaction(
|
||||
request, response, txn_id, include_legacy=legacy_csrf
|
||||
)
|
||||
self._set_consent_binding_cookie(request, response, txn_id, consent_token)
|
||||
return response
|
||||
|
|
@ -549,8 +742,9 @@ class ConsentMixin:
|
|||
max_age=365 * 24 * 3600,
|
||||
)
|
||||
|
||||
self._set_list_cookie(
|
||||
response, "MCP_CONSENT_STATE", self._encode_list_cookie([]), max_age=60
|
||||
await self._consent_csrf_store.delete(key=_hash_token(csrf_token))
|
||||
self._clear_consent_state_for_transaction(
|
||||
request, response, txn_id, include_legacy=legacy_csrf
|
||||
)
|
||||
return response
|
||||
|
||||
|
|
|
|||
|
|
@ -58,11 +58,32 @@ class OAuthTransaction(BaseModel):
|
|||
created_at: float
|
||||
resource: str | None = None
|
||||
proxy_code_verifier: str | None = None
|
||||
# Deprecated: consent CSRF tokens are now stored under their own keys (see
|
||||
# ConsentCSRFToken) so that concurrent renders cannot overwrite each other.
|
||||
# These two fields are only read, never written, and only to keep a consent
|
||||
# flow that started before the upgrade submittable after it.
|
||||
csrf_token: str | None = None
|
||||
csrf_expires_at: float | None = None
|
||||
consent_token: str | None = None
|
||||
|
||||
|
||||
class ConsentCSRFToken(BaseModel):
|
||||
"""One CSRF token issued for one render of the consent page.
|
||||
|
||||
Stored under a key derived from the token itself rather than on the
|
||||
transaction. Every render of a consent page issues its own token, and two
|
||||
renders can be in flight at once (a reload, a browser preload, an extension
|
||||
re-fetching the URL). Appending to a list on the transaction loses one of
|
||||
them whenever that happens: `AsyncKeyValue` has no compare-and-swap, so two
|
||||
handlers read the same transaction, each append their own token, and the
|
||||
second write drops the first. Giving each token its own key makes the
|
||||
writes independent, which holds across processes sharing one backend.
|
||||
"""
|
||||
|
||||
txn_id: str
|
||||
expires_at: float
|
||||
|
||||
|
||||
class ClientCode(BaseModel):
|
||||
"""Client authorization code with PKCE and upstream tokens.
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ from fastmcp.server.auth.oauth_proxy.models import (
|
|||
DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS,
|
||||
HTTP_TIMEOUT_SECONDS,
|
||||
ClientCode,
|
||||
ConsentCSRFToken,
|
||||
JTIMapping,
|
||||
OAuthTransaction,
|
||||
ProxyDCRClient,
|
||||
|
|
@ -397,8 +398,10 @@ class OAuthProxy(OAuthProvider, ConsentMixin):
|
|||
redirect_uri) in the same browser. Cross-site navigations are
|
||||
still prompted to block AS-in-the-middle attacks. Lower UX
|
||||
friction, but weaker protection than True.
|
||||
- "external": skip the built-in consent screen; consent is handled
|
||||
externally (e.g. by the upstream IdP or a custom login page).
|
||||
- "external": follow the same authorization path as False, but
|
||||
suppress the warning as an operator acknowledgment that equivalent
|
||||
consent and transaction-binding protections are enforced externally.
|
||||
FastMCP does not provide or verify those external protections.
|
||||
- False: skip consent entirely. SECURITY WARNING: only set to
|
||||
False for local development or testing environments.
|
||||
consent_csp_policy: Content Security Policy for the consent page.
|
||||
|
|
@ -647,6 +650,19 @@ class OAuthProxy(OAuthProvider, ConsentMixin):
|
|||
raise_on_validation_error=True,
|
||||
)
|
||||
|
||||
# Consent CSRF tokens, keyed by a hash of the token rather than by
|
||||
# transaction. Each render of a consent page writes its own key, so
|
||||
# renders that overlap cannot overwrite one another the way appending
|
||||
# to a list on the transaction would.
|
||||
self._consent_csrf_store: PydanticAdapter[ConsentCSRFToken] = PydanticAdapter[
|
||||
ConsentCSRFToken
|
||||
](
|
||||
key_value=self._client_storage,
|
||||
pydantic_model=ConsentCSRFToken,
|
||||
default_collection="mcp-consent-csrf-tokens",
|
||||
raise_on_validation_error=True,
|
||||
)
|
||||
|
||||
self._code_store: PydanticAdapter[ClientCode] = PydanticAdapter[ClientCode](
|
||||
key_value=self._client_storage,
|
||||
pydantic_model=ClientCode,
|
||||
|
|
|
|||
|
|
@ -43,8 +43,7 @@ class AsyncOAuth2Client:
|
|||
Drop-in replacement for the slice of authlib's `AsyncOAuth2Client` that
|
||||
`OAuthProxy` uses. Subclasses of `OAuthProxy` that override
|
||||
`_create_upstream_oauth_client` may return any object with the same
|
||||
`fetch_token`/`refresh_token`/`client_secret`/`aclose` surface (including
|
||||
an authlib client, if legacy httpx is installed in their environment).
|
||||
`fetch_token`/`refresh_token`/`client_secret`/`aclose` surface.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
|
|||
|
|
@ -306,8 +306,9 @@ class OIDCProxy(OAuthProxy):
|
|||
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
|
||||
When True, users see a consent screen before being redirected to the upstream IdP.
|
||||
When False, authorization proceeds directly without user confirmation.
|
||||
When "external", the built-in consent screen is skipped but no warning is
|
||||
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
|
||||
When "external", authorization follows the same direct path as False,
|
||||
but the warning is suppressed as an operator acknowledgment that
|
||||
equivalent protections are enforced externally.
|
||||
SECURITY WARNING: Only set to False for local development or testing environments.
|
||||
consent_csp_policy: Content Security Policy for the consent page.
|
||||
If None (default), uses the built-in CSP policy with appropriate directives.
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ class Auth0Provider(OIDCProxy):
|
|||
fallback_refresh_token_expiry_seconds: int | None = None,
|
||||
fastmcp_access_token_expiry_seconds: int | None = None,
|
||||
token_expiry_threshold_seconds: int = 0,
|
||||
enable_cimd: bool = True,
|
||||
) -> None:
|
||||
"""Initialize Auth0 OAuth provider.
|
||||
|
||||
|
|
@ -134,8 +135,9 @@ class Auth0Provider(OIDCProxy):
|
|||
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
|
||||
When True, users see a consent screen before being redirected to Auth0.
|
||||
When False, authorization proceeds directly without user confirmation.
|
||||
When "external", the built-in consent screen is skipped but no warning is
|
||||
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
|
||||
When "external", authorization follows the same direct path as False,
|
||||
but the warning is suppressed as an operator acknowledgment that
|
||||
equivalent protections are enforced externally.
|
||||
SECURITY WARNING: Only set to False for local development or testing environments.
|
||||
fallback_refresh_token_expiry_seconds: Lifetime for the FastMCP-issued
|
||||
refresh token when the upstream provider omits `refresh_expires_in`
|
||||
|
|
@ -148,6 +150,8 @@ class Auth0Provider(OIDCProxy):
|
|||
refresh gracefully (e.g. `mcp-remote`). See `OAuthProxy` for details.
|
||||
token_expiry_threshold_seconds: Number of seconds before actual expiry to
|
||||
treat a token as expired, refreshing early to avoid races. Defaults to 0.
|
||||
enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
|
||||
client IDs (default True). Set to False to disable.
|
||||
"""
|
||||
# Parse scopes if provided as string
|
||||
auth0_required_scopes = (
|
||||
|
|
@ -174,6 +178,7 @@ class Auth0Provider(OIDCProxy):
|
|||
fallback_refresh_token_expiry_seconds=fallback_refresh_token_expiry_seconds,
|
||||
fastmcp_access_token_expiry_seconds=fastmcp_access_token_expiry_seconds,
|
||||
token_expiry_threshold_seconds=token_expiry_threshold_seconds,
|
||||
enable_cimd=enable_cimd,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ class AWSCognitoProvider(OIDCProxy):
|
|||
fallback_refresh_token_expiry_seconds: int | None = None,
|
||||
fastmcp_access_token_expiry_seconds: int | None = None,
|
||||
token_expiry_threshold_seconds: int = 0,
|
||||
enable_cimd: bool = True,
|
||||
):
|
||||
"""Initialize AWS Cognito OAuth provider.
|
||||
|
||||
|
|
@ -174,8 +175,9 @@ class AWSCognitoProvider(OIDCProxy):
|
|||
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
|
||||
When True, users see a consent screen before being redirected to AWS Cognito.
|
||||
When False, authorization proceeds directly without user confirmation.
|
||||
When "external", the built-in consent screen is skipped but no warning is
|
||||
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
|
||||
When "external", authorization follows the same direct path as False,
|
||||
but the warning is suppressed as an operator acknowledgment that
|
||||
equivalent protections are enforced externally.
|
||||
SECURITY WARNING: Only set to False for local development or testing environments.
|
||||
fallback_refresh_token_expiry_seconds: Lifetime for the FastMCP-issued
|
||||
refresh token when the upstream provider omits `refresh_expires_in`
|
||||
|
|
@ -188,6 +190,8 @@ class AWSCognitoProvider(OIDCProxy):
|
|||
refresh gracefully (e.g. `mcp-remote`). See `OAuthProxy` for details.
|
||||
token_expiry_threshold_seconds: Number of seconds before actual expiry to
|
||||
treat a token as expired, refreshing early to avoid races. Defaults to 0.
|
||||
enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
|
||||
client IDs (default True). Set to False to disable.
|
||||
"""
|
||||
# Parse scopes if provided as string
|
||||
required_scopes_final = (
|
||||
|
|
@ -223,6 +227,7 @@ class AWSCognitoProvider(OIDCProxy):
|
|||
fallback_refresh_token_expiry_seconds=fallback_refresh_token_expiry_seconds,
|
||||
fastmcp_access_token_expiry_seconds=fastmcp_access_token_expiry_seconds,
|
||||
token_expiry_threshold_seconds=token_expiry_threshold_seconds,
|
||||
enable_cimd=enable_cimd,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
|
|
|
|||
|
|
@ -173,8 +173,9 @@ class AzureProvider(OAuthProxy):
|
|||
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
|
||||
When True, users see a consent screen before being redirected to Azure.
|
||||
When False, authorization proceeds directly without user confirmation.
|
||||
When "external", the built-in consent screen is skipped but no warning is
|
||||
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
|
||||
When "external", authorization follows the same direct path as False,
|
||||
but the warning is suppressed as an operator acknowledgment that
|
||||
equivalent protections are enforced externally.
|
||||
SECURITY WARNING: Only set to False for local development or testing environments.
|
||||
http_client: Optional httpx2.AsyncClient for connection pooling in JWKS fetches.
|
||||
When provided, the client is reused for JWT key fetches and the caller
|
||||
|
|
|
|||
|
|
@ -327,8 +327,9 @@ class ClerkProvider(OAuthProxy):
|
|||
into a 32-byte key. If not provided, the upstream client secret will be used to
|
||||
derive a 32-byte key using PBKDF2.
|
||||
require_authorization_consent: Whether to require user consent before authorizing
|
||||
clients (default True). When "external", the built-in consent screen is skipped
|
||||
but no warning is logged, indicating that consent is handled externally by Clerk.
|
||||
clients (default True). When "external", authorization follows the same direct
|
||||
path as False, but the warning is suppressed as an operator acknowledgment that
|
||||
equivalent protections are enforced externally.
|
||||
consent_csp_policy: Custom CSP policy for the consent page.
|
||||
extra_authorize_params: Additional parameters to forward to Clerk's authorization
|
||||
endpoint. Example: {"prompt": "login"} to force re-authentication.
|
||||
|
|
|
|||
|
|
@ -241,8 +241,9 @@ class DiscordProvider(OAuthProxy):
|
|||
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
|
||||
When True, users see a consent screen before being redirected to Discord.
|
||||
When False, authorization proceeds directly without user confirmation.
|
||||
When "external", the built-in consent screen is skipped but no warning is
|
||||
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
|
||||
When "external", authorization follows the same direct path as False,
|
||||
but the warning is suppressed as an operator acknowledgment that
|
||||
equivalent protections are enforced externally.
|
||||
SECURITY WARNING: Only set to False for local development or testing environments.
|
||||
http_client: Optional httpx2.AsyncClient for connection pooling in token verification.
|
||||
When provided, the client is reused across verify_token calls and the caller
|
||||
|
|
|
|||
|
|
@ -44,6 +44,15 @@ class GitHubTokenVerifier(TokenVerifier):
|
|||
GitHub OAuth tokens are opaque (not JWTs), so we verify them
|
||||
by calling GitHub's API to check if they're valid and get user info.
|
||||
|
||||
Warning:
|
||||
GitHub tokens carry no audience claim, so this verifier cannot tell
|
||||
which OAuth app (if any) a token was issued for — any valid GitHub
|
||||
credential, including a personal access token, will verify. Used
|
||||
inside `GitHubProvider` this is safe, because the proxy only ever
|
||||
checks tokens it obtained through its own OAuth flow. As a standalone
|
||||
verifier it authenticates "some GitHub user", not "a user of your
|
||||
app" — only use it that way if that is genuinely your access model.
|
||||
|
||||
Caching is disabled by default. Set ``cache_ttl_seconds`` to a positive
|
||||
integer to cache successful verification results and avoid repeated
|
||||
GitHub API calls for the same token.
|
||||
|
|
@ -257,8 +266,9 @@ class GitHubProvider(OAuthProxy):
|
|||
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
|
||||
When True, users see a consent screen before being redirected to GitHub.
|
||||
When False, authorization proceeds directly without user confirmation.
|
||||
When "external", the built-in consent screen is skipped but no warning is
|
||||
logged, indicating that consent is handled externally (e.g. by the upstream IdP).
|
||||
When "external", authorization follows the same direct path as False,
|
||||
but the warning is suppressed as an operator acknowledgment that
|
||||
equivalent protections are enforced externally.
|
||||
SECURITY WARNING: Only set to False for local development or testing environments.
|
||||
http_client: Optional httpx2.AsyncClient for connection pooling in token verification.
|
||||
When provided, the client is reused across verify_token calls and the caller
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ class GoogleTokenVerifier(TokenVerifier):
|
|||
required_scopes: list[str] | None = None,
|
||||
timeout_seconds: int = 10,
|
||||
http_client: httpx2.AsyncClient | None = None,
|
||||
audience: str | list[str] | None = None,
|
||||
):
|
||||
"""Initialize the Google token verifier.
|
||||
|
||||
|
|
@ -79,6 +80,12 @@ class GoogleTokenVerifier(TokenVerifier):
|
|||
http_client: Optional httpx2.AsyncClient for connection pooling. When provided,
|
||||
the client is reused across calls and the caller is responsible for its
|
||||
lifecycle. When None (default), a fresh client is created per call.
|
||||
audience: Expected `aud` value (your Google OAuth client ID) or list of
|
||||
allowed values. When set, tokens minted for any other OAuth client are
|
||||
rejected. When None (default), any valid Google token is accepted
|
||||
regardless of which OAuth client it was issued to — only appropriate
|
||||
when the token's provenance is guaranteed elsewhere (as in
|
||||
`GoogleProvider`, which obtains tokens through its own OAuth flow).
|
||||
"""
|
||||
normalized = (
|
||||
[_normalize_google_scope(s) for s in required_scopes]
|
||||
|
|
@ -88,6 +95,7 @@ class GoogleTokenVerifier(TokenVerifier):
|
|||
super().__init__(required_scopes=normalized)
|
||||
self.timeout_seconds = timeout_seconds
|
||||
self._http_client = http_client
|
||||
self.audience = audience
|
||||
|
||||
async def verify_token(self, token: str) -> AccessToken | None:
|
||||
"""Verify a Google OAuth token using the tokeninfo endpoint.
|
||||
|
|
@ -126,6 +134,18 @@ class GoogleTokenVerifier(TokenVerifier):
|
|||
logger.debug("Google tokeninfo missing 'aud' claim")
|
||||
return None
|
||||
|
||||
if self.audience is not None:
|
||||
allowed = (
|
||||
self.audience
|
||||
if isinstance(self.audience, list)
|
||||
else [self.audience]
|
||||
)
|
||||
if aud not in allowed:
|
||||
logger.debug(
|
||||
"Google token 'aud' does not match expected audience"
|
||||
)
|
||||
return None
|
||||
|
||||
# sub is required (unique Google user ID)
|
||||
sub = token_data.get("sub")
|
||||
if not sub:
|
||||
|
|
@ -290,8 +310,9 @@ class GoogleProvider(OAuthProxy):
|
|||
require_authorization_consent: Whether to require user consent before authorizing clients (default True).
|
||||
When True, users see a consent screen before being redirected to Google.
|
||||
When False, authorization proceeds directly without user confirmation.
|
||||
When "external", the built-in consent screen is skipped but no warning is
|
||||
logged, indicating that consent is handled externally (e.g. by Google's own consent).
|
||||
When "external", authorization follows the same direct path as False,
|
||||
but the warning is suppressed as an operator acknowledgment that
|
||||
equivalent protections are enforced externally.
|
||||
SECURITY WARNING: Only set to False for local development or testing environments.
|
||||
extra_authorize_params: Additional parameters to forward to Google's authorization endpoint.
|
||||
By default, GoogleProvider sets {"access_type": "offline", "prompt": "consent"} to ensure
|
||||
|
|
@ -337,6 +358,7 @@ class GoogleProvider(OAuthProxy):
|
|||
required_scopes=required_scopes_final,
|
||||
timeout_seconds=timeout_seconds,
|
||||
http_client=http_client,
|
||||
audience=client_id,
|
||||
)
|
||||
|
||||
# Set Google-specific defaults for extra authorize params
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import contextlib
|
|||
import json
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, TypeAlias, cast
|
||||
from typing import Any, Literal, TypeAlias, cast
|
||||
|
||||
import httpx2
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
|
@ -29,22 +29,30 @@ JWKKeyData: TypeAlias = dict[str, str | list[str]]
|
|||
SUPPORTED_JWS_HEADER_FIELDS = frozenset(JWS_HEADER_REGISTRY)
|
||||
|
||||
|
||||
def _import_key_for_algorithm(key: str | bytes | JWKKeyData, algorithm: str):
|
||||
def _key_type_for_algorithm(algorithm: str) -> Literal["oct", "RSA", "EC", "OKP"]:
|
||||
if algorithm.startswith("HS"):
|
||||
return jwk.import_key(key, "oct")
|
||||
return "oct"
|
||||
if algorithm.startswith(("RS", "PS")):
|
||||
return jwk.import_key(key, "RSA")
|
||||
return "RSA"
|
||||
if algorithm.startswith("ES"):
|
||||
return jwk.import_key(key, "EC")
|
||||
return "EC"
|
||||
if algorithm in {"EdDSA", "Ed25519", "Ed448"}:
|
||||
return "OKP"
|
||||
raise ValueError(f"Unsupported algorithm: {algorithm}.")
|
||||
|
||||
|
||||
def _import_key_for_algorithm(key: str | bytes | JWKKeyData, algorithm: str):
|
||||
return jwk.import_key(key, _key_type_for_algorithm(algorithm))
|
||||
|
||||
|
||||
def _jwk_to_pem(key_data: JWKKeyData) -> str:
|
||||
key_type = key_data.get("kty")
|
||||
if key_type == "RSA":
|
||||
return jwk.import_key(key_data, "RSA").as_pem().decode("utf-8")
|
||||
if key_type == "EC":
|
||||
return jwk.import_key(key_data, "EC").as_pem().decode("utf-8")
|
||||
if key_type == "OKP":
|
||||
return jwk.import_key(key_data, "OKP").as_pem().decode("utf-8")
|
||||
raise ValueError(f"Unsupported JWK key type: {key_type!r}")
|
||||
|
||||
|
||||
|
|
@ -72,6 +80,8 @@ class JWKData(TypedDict, total=False):
|
|||
alg: str # Algorithm (e.g., "RS256")
|
||||
n: str # Modulus (for RSA keys)
|
||||
e: str # Exponent (for RSA keys)
|
||||
crv: str # Curve name (for EC and OKP keys)
|
||||
x: str # Public key coordinate (for EC and OKP keys)
|
||||
x5c: list[str] # X.509 certificate chain (for JWKs)
|
||||
x5t: str # X.509 certificate thumbprint (for JWKs)
|
||||
|
||||
|
|
@ -194,10 +204,11 @@ def _looks_like_pem_public_key(key: str | bytes) -> bool:
|
|||
|
||||
class JWTVerifier(TokenVerifier):
|
||||
"""
|
||||
JWT token verifier supporting both asymmetric (RSA/ECDSA) and symmetric (HMAC) algorithms.
|
||||
JWT token verifier supporting asymmetric (RSA/ECDSA/EdDSA) and symmetric (HMAC) algorithms.
|
||||
|
||||
This verifier validates JWT tokens using various signing algorithms:
|
||||
- **Asymmetric algorithms** (RS256/384/512, ES256/384/512, PS256/384/512):
|
||||
- **Asymmetric algorithms** (RS256/384/512, ES256/384/512, PS256/384/512,
|
||||
Ed25519, Ed448, and legacy EdDSA):
|
||||
Uses public/private key pairs. Ideal for external clients and services where
|
||||
only the authorization server has the private key.
|
||||
- **Symmetric algorithms** (HS256/384/512): Uses a shared secret for both
|
||||
|
|
@ -232,7 +243,7 @@ class JWTVerifier(TokenVerifier):
|
|||
jwks_uri: URI to fetch a JSON Web Key Set; used when verifying tokens with remote JWKS.
|
||||
issuer: Expected issuer claim value or list of allowed issuer values.
|
||||
audience: Expected audience claim value or list of allowed audience values.
|
||||
algorithm: JWT signing algorithm to accept (default: "RS256"). Supported: HS256/384/512, RS256/384/512, ES256/384/512, PS256/384/512.
|
||||
algorithm: JWT signing algorithm to accept (default: "RS256"). Supported: HS256/384/512, RS256/384/512, ES256/384/512, PS256/384/512, Ed25519, Ed448, and legacy EdDSA.
|
||||
required_scopes: Scopes that must be present in validated tokens.
|
||||
base_url: Base URL passed to the parent TokenVerifier.
|
||||
ssrf_safe: If True, JWKS fetches use SSRF protection (HTTPS-only,
|
||||
|
|
@ -275,6 +286,9 @@ class JWTVerifier(TokenVerifier):
|
|||
"PS256",
|
||||
"PS384",
|
||||
"PS512",
|
||||
"EdDSA",
|
||||
"Ed25519",
|
||||
"Ed448",
|
||||
}:
|
||||
raise ValueError(f"Unsupported algorithm: {algorithm}.")
|
||||
|
||||
|
|
@ -347,19 +361,31 @@ class JWTVerifier(TokenVerifier):
|
|||
try:
|
||||
jwks_data = await self._fetch_jwks()
|
||||
|
||||
# Cache all usable keys. A key that cannot be converted (e.g. an
|
||||
# unsupported kty like OKP/Ed25519) is skipped rather than failing
|
||||
# the whole set — per RFC 7517 §5, clients should ignore JWKs they
|
||||
# don't understand. Otherwise one exotic key published by the
|
||||
# authorization server would reject every token, including ones
|
||||
# signed by supported keys in the same set (#4515).
|
||||
# Cache all usable keys. A key that cannot be converted is skipped
|
||||
# rather than failing the whole set — per RFC 7517 §5, clients
|
||||
# should ignore JWKs they don't understand. Otherwise one exotic
|
||||
# key published by the authorization server would reject every
|
||||
# token, including ones signed by supported keys in the same set
|
||||
# (#4515).
|
||||
self._jwks_cache = {}
|
||||
skipped_kids: set[str] = set()
|
||||
expected_key_type = _key_type_for_algorithm(self.algorithm)
|
||||
for key_data in jwks_data.get("keys", []):
|
||||
if not isinstance(key_data, dict):
|
||||
self.logger.debug("Skipping non-object JWKS entry: %r", key_data)
|
||||
continue
|
||||
key_kid = key_data.get("kid")
|
||||
if key_data.get("kty") != expected_key_type:
|
||||
self.logger.debug(
|
||||
"Skipping JWKS key %r: key type %r is incompatible "
|
||||
"with algorithm %s",
|
||||
key_kid,
|
||||
key_data.get("kty"),
|
||||
self.algorithm,
|
||||
)
|
||||
if key_kid:
|
||||
skipped_kids.add(key_kid)
|
||||
continue
|
||||
try:
|
||||
public_key = _jwk_to_pem(key_data)
|
||||
except (JoseError, TypeError, KeyError, ValueError) as e:
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ class OCIProvider(OIDCProxy):
|
|||
fallback_refresh_token_expiry_seconds: int | None = None,
|
||||
fastmcp_access_token_expiry_seconds: int | None = None,
|
||||
token_expiry_threshold_seconds: int = 0,
|
||||
enable_cimd: bool = True,
|
||||
) -> None:
|
||||
"""Initialize OCI OIDC provider.
|
||||
|
||||
|
|
@ -174,6 +175,8 @@ class OCIProvider(OIDCProxy):
|
|||
refresh gracefully (e.g. `mcp-remote`). See `OAuthProxy` for details.
|
||||
token_expiry_threshold_seconds: Number of seconds before actual expiry to
|
||||
treat a token as expired, refreshing early to avoid races. Defaults to 0.
|
||||
enable_cimd: Enable CIMD (Client ID Metadata Document) support for URL-based
|
||||
client IDs (default True). Set to False to disable.
|
||||
"""
|
||||
# Parse scopes if provided as string
|
||||
oci_required_scopes = (
|
||||
|
|
@ -200,6 +203,7 @@ class OCIProvider(OIDCProxy):
|
|||
fallback_refresh_token_expiry_seconds=fallback_refresh_token_expiry_seconds,
|
||||
fastmcp_access_token_expiry_seconds=fastmcp_access_token_expiry_seconds,
|
||||
token_expiry_threshold_seconds=token_expiry_threshold_seconds,
|
||||
enable_cimd=enable_cimd,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
|
|
|
|||
|
|
@ -125,15 +125,22 @@ class ScalekitProvider(RemoteAuthProvider):
|
|||
|
||||
# Create default JWT verifier if none provided
|
||||
if token_verifier is None:
|
||||
# Scalekit is migrating the `iss` claim from the bare environment URL
|
||||
# to a resource-scoped issuer. Accept both forms so tokens minted
|
||||
# before and after the migration validate against the same provider.
|
||||
expected_issuers = [
|
||||
self.environment_url,
|
||||
f"{self.environment_url}/resources/{self.resource_id}",
|
||||
]
|
||||
logger.debug(
|
||||
"Creating default JWTVerifier for Scalekit: jwks_uri=%s issuer=%s required_scopes=%s",
|
||||
f"{self.environment_url}/keys",
|
||||
self.environment_url,
|
||||
expected_issuers,
|
||||
self.required_scopes,
|
||||
)
|
||||
token_verifier = JWTVerifier(
|
||||
jwks_uri=f"{self.environment_url}/keys",
|
||||
issuer=self.environment_url,
|
||||
issuer=expected_issuers,
|
||||
algorithm="RS256",
|
||||
audience=self.resource_id,
|
||||
required_scopes=self.required_scopes or None,
|
||||
|
|
|
|||
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